diff --git a/.coveragerc b/.coveragerc
index 3785240a387b06..8d3aeb916d1164 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -33,7 +33,11 @@ omit =
homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/cover.py
- homeassistant/components/alarmdecoder/*
+ homeassistant/components/alarmdecoder/__init__.py
+ homeassistant/components/alarmdecoder/alarm_control_panel.py
+ homeassistant/components/alarmdecoder/binary_sensor.py
+ homeassistant/components/alarmdecoder/const.py
+ homeassistant/components/alarmdecoder/sensor.py
homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/tts.py
homeassistant/components/ambiclimate/climate.py
@@ -117,7 +121,6 @@ omit =
homeassistant/components/buienradar/util.py
homeassistant/components/buienradar/weather.py
homeassistant/components/caldav/calendar.py
- homeassistant/components/canary/alarm_control_panel.py
homeassistant/components/canary/camera.py
homeassistant/components/cast/*
homeassistant/components/cert_expiry/helper.py
@@ -266,7 +269,9 @@ omit =
homeassistant/components/firmata/board.py
homeassistant/components/firmata/const.py
homeassistant/components/firmata/entity.py
+ homeassistant/components/firmata/light.py
homeassistant/components/firmata/pin.py
+ homeassistant/components/firmata/sensor.py
homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/sensor.py
homeassistant/components/fixer/sensor.py
@@ -315,6 +320,8 @@ omit =
homeassistant/components/glances/sensor.py
homeassistant/components/gntp/notify.py
homeassistant/components/goalfeed/*
+ homeassistant/components/goalzero/__init__.py
+ homeassistant/components/goalzero/binary_sensor.py
homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
@@ -369,6 +376,7 @@ omit =
homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/cover.py
homeassistant/components/hunterdouglas_powerview/entity.py
+ homeassistant/components/hvv_departures/binary_sensor.py
homeassistant/components/hvv_departures/sensor.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hydrawise/*
@@ -478,7 +486,8 @@ omit =
homeassistant/components/london_underground/sensor.py
homeassistant/components/loopenergy/sensor.py
homeassistant/components/luci/device_tracker.py
- homeassistant/components/luftdaten/*
+ homeassistant/components/luftdaten/__init__.py
+ homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/*
homeassistant/components/lutron/*
homeassistant/components/lutron_caseta/__init__.py
@@ -530,7 +539,9 @@ omit =
homeassistant/components/mjpeg/camera.py
homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
- homeassistant/components/modbus/*
+ homeassistant/components/modbus/climate.py
+ homeassistant/components/modbus/cover.py
+ homeassistant/components/modbus/switch.py
homeassistant/components/modem_callerid/sensor.py
homeassistant/components/mpchc/media_player.py
homeassistant/components/mpd/media_player.py
@@ -595,6 +606,10 @@ omit =
homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/ohmconnect/sensor.py
homeassistant/components/ombi/*
+ homeassistant/components/omnilogic/__init__.py
+ homeassistant/components/omnilogic/common.py
+ homeassistant/components/omnilogic/sensor.py
+ homeassistant/components/onewire/const.py
homeassistant/components/onewire/sensor.py
homeassistant/components/onkyo/media_player.py
homeassistant/components/onvif/__init__.py
@@ -636,6 +651,9 @@ 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
@@ -803,6 +821,7 @@ omit =
homeassistant/components/spc/*
homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/*
+ homeassistant/components/splunk/*
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/__init__.py
homeassistant/components/spotify/media_player.py
@@ -829,9 +848,10 @@ omit =
homeassistant/components/synology/camera.py
homeassistant/components/synology_chat/notify.py
homeassistant/components/synology_dsm/__init__.py
- homeassistant/components/synology_dsm/camera.py
homeassistant/components/synology_dsm/binary_sensor.py
+ homeassistant/components/synology_dsm/camera.py
homeassistant/components/synology_dsm/sensor.py
+ homeassistant/components/synology_dsm/switch.py
homeassistant/components/synology_srm/device_tracker.py
homeassistant/components/syslog/notify.py
homeassistant/components/systemmonitor/sensor.py
@@ -845,7 +865,13 @@ omit =
homeassistant/components/ted5000/sensor.py
homeassistant/components/telegram/notify.py
homeassistant/components/telegram_bot/*
- homeassistant/components/tellduslive/*
+ homeassistant/components/tellduslive/__init__.py
+ homeassistant/components/tellduslive/binary_sensor.py
+ homeassistant/components/tellduslive/cover.py
+ homeassistant/components/tellduslive/entry.py
+ homeassistant/components/tellduslive/light.py
+ homeassistant/components/tellduslive/sensor.py
+ homeassistant/components/tellduslive/switch.py
homeassistant/components/tellstick/*
homeassistant/components/telnet/switch.py
homeassistant/components/temper/sensor.py
@@ -864,7 +890,9 @@ omit =
homeassistant/components/thingspeak/*
homeassistant/components/thinkingcleaner/*
homeassistant/components/thomson/device_tracker.py
- homeassistant/components/tibber/*
+ homeassistant/components/tibber/__init__.py
+ homeassistant/components/tibber/notify.py
+ homeassistant/components/tibber/sensor.py
homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py
homeassistant/components/tile/device_tracker.py
@@ -1025,12 +1053,8 @@ omit =
homeassistant/components/zhong_hong/climate.py
homeassistant/components/xbee/*
homeassistant/components/ziggo_mediabox_xl/media_player.py
- homeassistant/components/zoneminder/*
homeassistant/components/supla/*
homeassistant/components/zwave/util.py
- homeassistant/components/ozw/__init__.py
- homeassistant/components/ozw/entity.py
- homeassistant/components/ozw/services.py
[report]
# Regexes for lines to exclude from consideration
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3d65df477e7605..600a94cec66605 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
@@ -73,7 +73,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -118,7 +118,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -163,7 +163,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -230,7 +230,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -278,7 +278,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -326,7 +326,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -371,7 +371,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -419,7 +419,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -475,7 +475,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -555,7 +555,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2.1.3
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -736,7 +736,7 @@ jobs:
-p no:sugar \
tests
- name: Upload coverage artifact
- uses: actions/upload-artifact@v2.1.4
+ uses: actions/upload-artifact@v2.2.0
with:
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
path: .coverage
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e24b6095b4a011..121cc1eab8e900 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ repos:
- --quiet-level=2
exclude_types: [csv, json]
- repo: https://gitlab.com/pycqa/flake8
- rev: 3.8.3
+ rev: 3.8.4
hooks:
- id: flake8
additional_dependencies:
@@ -39,7 +39,7 @@ repos:
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
- rev: 5.5.2
+ rev: 5.5.3
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/CODEOWNERS b/CODEOWNERS
index d6bb7042c41280..a335dc0bb19a46 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -157,6 +157,7 @@ homeassistant/components/geonetnz_volcano/* @exxamalte
homeassistant/components/gios/* @bieniu
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff @engrbm87
+homeassistant/components/goalzero/* @tkdrob
homeassistant/components/gogogate2/* @vangorra
homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton
@@ -169,6 +170,7 @@ homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
homeassistant/components/hassio/* @home-assistant/hass-io
+homeassistant/components/hdmi_cec/* @newAM
homeassistant/components/heatmiser/* @andylockran
homeassistant/components/heos/* @andrewsayre
homeassistant/components/here_travel_time/* @eifinger
@@ -193,6 +195,7 @@ homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion
homeassistant/components/hydrawise/* @ptcryan
+homeassistant/components/hyperion/* @dermotduffy
homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame
@@ -226,7 +229,7 @@ homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
-homeassistant/components/kodi/* @OnFreund
+homeassistant/components/kodi/* @OnFreund @cgtobi
homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
@@ -238,7 +241,7 @@ homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/loopenergy/* @pavoni
homeassistant/components/lovelace/* @home-assistant/frontend
-homeassistant/components/luci/* @fbradyirl @mzdrale
+homeassistant/components/luci/* @mzdrale
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/lupusec/* @majuss
homeassistant/components/lutron/* @JonGilmore
@@ -261,7 +264,7 @@ homeassistant/components/min_max/* @fabaff
homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
-homeassistant/components/modbus/* @adamchengtkc @janiversen
+homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff
homeassistant/components/mpd/* @fabaff
@@ -300,6 +303,7 @@ homeassistant/components/nzbget/* @chriscla
homeassistant/components/obihai/* @dshokouhi
homeassistant/components/ohmconnect/* @robbiet480
homeassistant/components/ombi/* @larssont
+homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu
homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/onewire/* @garbled1
homeassistant/components/onvif/* @hunterjm
@@ -330,6 +334,7 @@ homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
homeassistant/components/point/* @fredrike
homeassistant/components/poolsense/* @haemishkyd
homeassistant/components/powerwall/* @bdraco @jrester
+homeassistant/components/profiler/* @bdraco
homeassistant/components/progettihwsw/* @ardaseremet
homeassistant/components/prometheus/* @knyar
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
@@ -349,14 +354,16 @@ homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
+homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/repetier/* @MTrab
-homeassistant/components/rfxtrx/* @danielhiversen @elupus
+homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
homeassistant/components/ring/* @balloob
homeassistant/components/risco/* @OnFreund
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roku/* @ctalkington
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
homeassistant/components/roon/* @pavoni
+homeassistant/components/rpi_power/* @shenxn @swetoast
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
homeassistant/components/salt/* @bjornorri
@@ -399,9 +406,11 @@ homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn
+homeassistant/components/sonos/* @cgtobi
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
homeassistant/components/spider/* @peternijssen
+homeassistant/components/splunk/* @Bre77
homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes
homeassistant/components/squeezebox/* @rajlaud
@@ -428,6 +437,7 @@ homeassistant/components/tado/* @michaelarnauts @bdraco
homeassistant/components/tag/* @balloob @dmulcahey
homeassistant/components/tahoma/* @philklei
homeassistant/components/tankerkoenig/* @guillempages
+homeassistant/components/tasmota/* @emontnemery
homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike
homeassistant/components/template/* @PhracturedBlue @tetienne
@@ -466,7 +476,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra
homeassistant/components/velux/* @Julius2342
homeassistant/components/vera/* @vangorra
homeassistant/components/versasense/* @flamm3blemuff1n
-homeassistant/components/version/* @fabaff
+homeassistant/components/version/* @fabaff @ludeeus
homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey
homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW
@@ -502,8 +512,9 @@ homeassistant/components/yi/* @bachya
homeassistant/components/zeroconf/* @Kane610
homeassistant/components/zerproc/* @emlove
homeassistant/components/zha/* @dmulcahey @adminiuga
+homeassistant/components/zodiac/* @JulienTant
homeassistant/components/zone/* @home-assistant/core
-homeassistant/components/zoneminder/* @rohankapoorcom
+homeassistant/components/zoneminder/* @rohankapoorcom @vangorra
homeassistant/components/zwave/* @home-assistant/z-wave
# Individual files
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1921e5d38dd146..8f8a79ab901676 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,14 +5,14 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
- - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
+ - Fork the Home Assistant [git repository](https://github.com/home-assistant/core).
- Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work.
- - Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
+ - Create a Pull Request against the [**dev**](https://github.com/home-assistant/core/tree/dev) branch of Home Assistant.
Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.
## Feature suggestions
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
-We use [GitHub for tracking issues](https://github.com/home-assistant/home-assistant/issues), not for tracking feature requests.
\ No newline at end of file
+We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests.
diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml
index 77d90ac94f107e..bcf16e8dee764b 100644
--- a/azure-pipelines-wheels.yml
+++ b/azure-pipelines-wheels.yml
@@ -47,7 +47,7 @@ jobs:
- template: templates/azp-job-wheels.yaml@azure
parameters:
builderVersion: '$(versionWheels)'
- builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
+ builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
builderPip: 'Cython;numpy;scikit-build'
builderEnvFile: true
skipBinary: 'aiohttp'
diff --git a/build.json b/build.json
index 6d8763a019f196..d63a245793b480 100644
--- a/build.json
+++ b/build.json
@@ -1,11 +1,11 @@
{
"image": "homeassistant/{arch}-homeassistant",
"build_from": {
- "aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0",
- "armhf": "homeassistant/armhf-homeassistant-base:8.3.0",
- "armv7": "homeassistant/armv7-homeassistant-base:8.3.0",
- "amd64": "homeassistant/amd64-homeassistant-base:8.3.0",
- "i386": "homeassistant/i386-homeassistant-base:8.3.0"
+ "aarch64": "homeassistant/aarch64-homeassistant-base:8.4.0",
+ "armhf": "homeassistant/armhf-homeassistant-base:8.4.0",
+ "armv7": "homeassistant/armv7-homeassistant-base:8.4.0",
+ "amd64": "homeassistant/amd64-homeassistant-base:8.4.0",
+ "i386": "homeassistant/i386-homeassistant-base:8.4.0"
},
"labels": {
"io.hass.type": "core"
diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html
index 53a8d1e425d702..7982649f72ed8b 100644
--- a/docs/source/_templates/links.html
+++ b/docs/source/_templates/links.html
@@ -1,6 +1,6 @@
diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py
index 18146551a56934..72e7ec1d9ebafd 100644
--- a/homeassistant/components/abode/config_flow.py
+++ b/homeassistant/components/abode/config_flow.py
@@ -47,8 +47,8 @@ async def async_step_user(self, user_input=None):
except (AbodeException, ConnectTimeout, HTTPError) as ex:
LOGGER.error("Unable to connect to Abode: %s", str(ex))
if ex.errcode == HTTP_BAD_REQUEST:
- return self._show_form({"base": "invalid_credentials"})
- return self._show_form({"base": "connection_error"})
+ return self._show_form({"base": "invalid_auth"})
+ return self._show_form({"base": "cannot_connect"})
return self.async_create_entry(
title=user_input[CONF_USERNAME],
diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json
index 14d570e76e2cb3..63b62fefceccf2 100644
--- a/homeassistant/components/abode/strings.json
+++ b/homeassistant/components/abode/strings.json
@@ -10,12 +10,11 @@
}
},
"error": {
- "identifier_exists": "Account already registered.",
- "invalid_credentials": "Invalid credentials.",
- "connection_error": "Unable to connect to Abode."
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Abode is allowed."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/ca.json b/homeassistant/components/abode/translations/ca.json
index 5a1552700d9900..f33b51f6d6bd82 100644
--- a/homeassistant/components/abode/translations/ca.json
+++ b/homeassistant/components/abode/translations/ca.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'Abode."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No es pot connectar amb Abode.",
"identifier_exists": "Compte ja registrat.",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Credencials inv\u00e0lides."
},
"step": {
diff --git a/homeassistant/components/abode/translations/el.json b/homeassistant/components/abode/translations/el.json
new file mode 100644
index 00000000000000..b30be708065e42
--- /dev/null
+++ b/homeassistant/components/abode/translations/el.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/en.json b/homeassistant/components/abode/translations/en.json
index ae33c6bed04c7b..34a1ad27484216 100644
--- a/homeassistant/components/abode/translations/en.json
+++ b/homeassistant/components/abode/translations/en.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Only a single configuration of Abode is allowed."
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Unable to connect to Abode.",
"identifier_exists": "Account already registered.",
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid credentials."
},
"step": {
diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json
index 76f06de9b85483..9999856883c9e0 100644
--- a/homeassistant/components/abode/translations/es.json
+++ b/homeassistant/components/abode/translations/es.json
@@ -4,8 +4,10 @@
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se puede conectar a Abode.",
"identifier_exists": "Cuenta ya registrada.",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Credenciales inv\u00e1lidas."
},
"step": {
diff --git a/homeassistant/components/abode/translations/et.json b/homeassistant/components/abode/translations/et.json
new file mode 100644
index 00000000000000..de5254de79674c
--- /dev/null
+++ b/homeassistant/components/abode/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json
index 1c4cfe0087256f..81056f44fa81c6 100644
--- a/homeassistant/components/abode/translations/fr.json
+++ b/homeassistant/components/abode/translations/fr.json
@@ -4,8 +4,10 @@
"single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter \u00e0 Abode.",
"identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9.",
+ "invalid_auth": "Authentification invalide",
"invalid_credentials": "Informations d'identification invalides."
},
"step": {
diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json
index 97b5d56228389d..e6a43a31d0d352 100644
--- a/homeassistant/components/abode/translations/it.json
+++ b/homeassistant/components/abode/translations/it.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u00c8 consentita una sola configurazione di Abode."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi ad Abode.",
"identifier_exists": "Account gi\u00e0 registrato",
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Credenziali non valide"
},
"step": {
diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json
index 60c1ad897f14b5..af6f3ed27493b2 100644
--- a/homeassistant/components/abode/translations/no.json
+++ b/homeassistant/components/abode/translations/no.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Bare en enkelt konfigurasjon av Abode er tillatt."
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kan ikke koble til Abode.",
"identifier_exists": "Kontoen er allerede registrert.",
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_credentials": "Ugyldig legitimasjon"
},
"step": {
diff --git a/homeassistant/components/abode/translations/pl.json b/homeassistant/components/abode/translations/pl.json
index d7a25bb20b7621..7ff6317559cd03 100644
--- a/homeassistant/components/abode/translations/pl.json
+++ b/homeassistant/components/abode/translations/pl.json
@@ -4,6 +4,7 @@
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Abode."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.",
"identifier_exists": "Konto jest ju\u017c zarejestrowane.",
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
diff --git a/homeassistant/components/abode/translations/ru.json b/homeassistant/components/abode/translations/ru.json
index e0e6e131289da7..bec110bb70623a 100644
--- a/homeassistant/components/abode/translations/ru.json
+++ b/homeassistant/components/abode/translations/ru.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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": {
+ "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 \u043a Abode.",
"identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"step": {
diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json
index 7a97cbda3c59f6..760935accd74aa 100644
--- a/homeassistant/components/abode/translations/zh-Hant.json
+++ b/homeassistant/components/abode/translations/zh-Hant.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Abode\u3002"
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Abode\u3002",
"identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a\u3002",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_credentials": "\u6191\u8b49\u7121\u6548\u3002"
},
"step": {
diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py
index e572feafcf81af..5696a35ea2f542 100644
--- a/homeassistant/components/accuweather/const.py
+++ b/homeassistant/components/accuweather/const.py
@@ -5,6 +5,7 @@
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_METERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
@@ -24,7 +25,6 @@
CONCENTRATION_PARTS_PER_CUBIC_METER = f"p/{VOLUME_CUBIC_METERS}"
COORDINATOR = "coordinator"
DOMAIN = "accuweather"
-LENGTH_MILIMETERS = "mm"
MANUFACTURER = "AccuWeather, Inc."
NAME = "AccuWeather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
@@ -238,7 +238,7 @@
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-rainy",
ATTR_LABEL: "Precipitation",
- ATTR_UNIT_METRIC: LENGTH_MILIMETERS,
+ ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
},
"PressureTendency": {
diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json
index a383c49f34893d..6ccd6a4f10b1f5 100644
--- a/homeassistant/components/accuweather/manifest.json
+++ b/homeassistant/components/accuweather/manifest.json
@@ -2,7 +2,7 @@
"domain": "accuweather",
"name": "AccuWeather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
- "requirements": ["accuweather==0.0.10"],
+ "requirements": ["accuweather==0.0.11"],
"codeowners": ["@bieniu"],
"config_flow": true,
"quality_scale": "platinum"
diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json
index 89228cd069240e..7bf61de8476c08 100644
--- a/homeassistant/components/accuweather/strings.json
+++ b/homeassistant/components/accuweather/strings.json
@@ -5,10 +5,10 @@
"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": "Name of the integration",
+ "name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json
new file mode 100644
index 00000000000000..320c834e920cbb
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/de.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad"
+ },
+ "title": "AccuWeather"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json
new file mode 100644
index 00000000000000..f669c36ad39273
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/et.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sidumine juba tehtud. V\u00f5imalik on ainult 1 sidumine."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendus eba\u00f5nnestus",
+ "invalid_api_key": "API v\u00f5ti on vale",
+ "requests_exceeded": "Accuweatheri API-le esitatud p\u00e4ringute piirarv on \u00fcletatud. Peate ootama (v\u00f5i muutma API v\u00f5tit)."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Sidumise nimi"
+ },
+ "description": "Kui vajate seadistamisel abi vaadake siit: https://www.home-assistant.io/integrations/accuweather/ \n\n M\u00f5ni andur pole vaikimisi lubatud. P\u00e4rast sidumise seadistamist saate need \u00fcksused lubada. \n Ilmapennustus pole vaikimisi lubatud. Saate selle lubada sidumise s\u00e4tetes.",
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "Ilmateade"
+ },
+ "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.",
+ "title": "AccuWeatheri valikud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json
index 33001cf5b84463..40cf1ccc0b9f9f 100644
--- a/homeassistant/components/accuweather/translations/fr.json
+++ b/homeassistant/components/accuweather/translations/fr.json
@@ -5,11 +5,30 @@
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
- "invalid_api_key": "Cl\u00e9 API invalide"
+ "invalid_api_key": "Cl\u00e9 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": {
"user": {
- "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration."
+ "data": {
+ "api_key": "Cl\u00e9 d'API",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom de l'int\u00e9gration"
+ },
+ "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.",
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques"
+ },
+ "description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes.",
+ "title": "Options AccuWeather"
}
}
}
diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json
index a518c287b1103f..052ed5b6236133 100644
--- a/homeassistant/components/accuweather/translations/pl.json
+++ b/homeassistant/components/accuweather/translations/pl.json
@@ -4,7 +4,7 @@
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
"requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API AccuWeather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API."
},
diff --git a/homeassistant/components/accuweather/translations/sensor.et.json b/homeassistant/components/accuweather/translations/sensor.et.json
new file mode 100644
index 00000000000000..ca58cd9ab6bcb3
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.et.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "Langev",
+ "rising": "T\u00f5usev",
+ "steady": "\u00dchtlane"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json
index a399c8f0694058..8c3f282b35070d 100644
--- a/homeassistant/components/accuweather/translations/uk.json
+++ b/homeassistant/components/accuweather/translations/uk.json
@@ -6,7 +6,6 @@
"step": {
"user": {
"data": {
- "api_key": "",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457"
diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py
index 816768b0800c35..f421fa9ca25329 100644
--- a/homeassistant/components/acmeda/config_flow.py
+++ b/homeassistant/components/acmeda/config_flow.py
@@ -45,7 +45,7 @@ async def async_step_user(self, user_input=None):
pass
if len(hubs) == 0:
- return self.async_abort(reason="all_configured")
+ return self.async_abort(reason="no_devices_found")
if len(hubs) == 1:
return await self.async_create(hubs[0])
diff --git a/homeassistant/components/acmeda/strings.json b/homeassistant/components/acmeda/strings.json
index eb7ed44999b2db..a2209fe1a88ab1 100644
--- a/homeassistant/components/acmeda/strings.json
+++ b/homeassistant/components/acmeda/strings.json
@@ -10,7 +10,7 @@
}
},
"abort": {
- "all_configured": "No new Pulse hubs discovered."
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/acmeda/translations/ca.json b/homeassistant/components/acmeda/translations/ca.json
index 0812387ab7f33c..2481794f8f2782 100644
--- a/homeassistant/components/acmeda/translations/ca.json
+++ b/homeassistant/components/acmeda/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "No s'han descobert nous hubs de Pulse."
+ "all_configured": "No s'han descobert nous hubs de Pulse.",
+ "no_devices_found": "No s'han trobat dispositius a la xarxa"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/en.json b/homeassistant/components/acmeda/translations/en.json
index ab20d0b1939df6..77768f20d69b43 100644
--- a/homeassistant/components/acmeda/translations/en.json
+++ b/homeassistant/components/acmeda/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "No new Pulse hubs discovered."
+ "all_configured": "No new Pulse hubs discovered.",
+ "no_devices_found": "No devices found on the network"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/es.json b/homeassistant/components/acmeda/translations/es.json
index 0ca3dbf6e2ff39..dcdd885fe26e3c 100644
--- a/homeassistant/components/acmeda/translations/es.json
+++ b/homeassistant/components/acmeda/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "No se han descubierto nuevos hubs Pulse."
+ "all_configured": "No se han descubierto nuevos hubs Pulse.",
+ "no_devices_found": "No se encontraron dispositivos en la red"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/et.json b/homeassistant/components/acmeda/translations/et.json
new file mode 100644
index 00000000000000..b6075037dc465c
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "V\u00f5rgus ei tuvastatud \u00fchtegi seadet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/translations/fr.json b/homeassistant/components/acmeda/translations/fr.json
index a60d52ac6f44c0..8270b13d76e7e5 100644
--- a/homeassistant/components/acmeda/translations/fr.json
+++ b/homeassistant/components/acmeda/translations/fr.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "Aucun nouveau hub Pulse n'a \u00e9t\u00e9 d\u00e9couvert."
+ "all_configured": "Aucun nouveau hub Pulse n'a \u00e9t\u00e9 d\u00e9couvert.",
+ "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/it.json b/homeassistant/components/acmeda/translations/it.json
index a9349d0692384b..dbf92d54db92b2 100644
--- a/homeassistant/components/acmeda/translations/it.json
+++ b/homeassistant/components/acmeda/translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "Non sono stati scoperti nuovi hub Pulse."
+ "all_configured": "Non sono stati scoperti nuovi hub Pulse.",
+ "no_devices_found": "Nessun dispositivo trovato sulla rete"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json
index 66335077cfbaff..8ecb4ef889dd51 100644
--- a/homeassistant/components/acmeda/translations/no.json
+++ b/homeassistant/components/acmeda/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "Ingen nye Pulse-hub oppdaget."
+ "all_configured": "Ingen nye Pulse-hub oppdaget.",
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket"
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/ru.json b/homeassistant/components/acmeda/translations/ru.json
index 92922fdbb5d99b..b11fd876975153 100644
--- a/homeassistant/components/acmeda/translations/ru.json
+++ b/homeassistant/components/acmeda/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b."
+ "all_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.",
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/acmeda/translations/zh-Hant.json b/homeassistant/components/acmeda/translations/zh-Hant.json
index 5d4263ea5ae098..e5f29fbb4bdf04 100644
--- a/homeassistant/components/acmeda/translations/zh-Hant.json
+++ b/homeassistant/components/acmeda/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "all_configured": "\u672a\u641c\u5c0b\u5230 Pulse hub"
+ "all_configured": "\u672a\u641c\u5c0b\u5230 Pulse hub",
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099"
},
"step": {
"user": {
diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py
index a0ace62386291d..c799c4a71355c8 100644
--- a/homeassistant/components/adguard/config_flow.py
+++ b/homeassistant/components/adguard/config_flow.py
@@ -80,7 +80,7 @@ async def async_step_user(self, user_input=None):
try:
await adguard.version()
except AdGuardHomeConnectionError:
- errors["base"] = "connection_error"
+ errors["base"] = "cannot_connect"
return await self._show_setup_form(errors)
return self.async_create_entry(
@@ -152,7 +152,7 @@ async def async_step_hassio_confirm(self, user_input=None):
try:
await adguard.version()
except AdGuardHomeConnectionError:
- errors["base"] = "connection_error"
+ errors["base"] = "cannot_connect"
return await self._show_hassio_form(errors)
return self.async_create_entry(
diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json
index f010f9e2ade25a..2d4bb49304fbe5 100644
--- a/homeassistant/components/adguard/strings.json
+++ b/homeassistant/components/adguard/strings.json
@@ -8,8 +8,8 @@
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
- "ssl": "AdGuard Home uses a SSL certificate",
- "verify_ssl": "AdGuard Home uses a proper certificate"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"hassio_confirm": {
@@ -17,10 +17,12 @@
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?"
}
},
- "error": { "connection_error": "Failed to connect." },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
"abort": {
"existing_instance_updated": "Updated existing configuration.",
- "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json
index b263e433cfb508..aec7f6fc2dab18 100644
--- a/homeassistant/components/adguard/translations/ca.json
+++ b/homeassistant/components/adguard/translations/ca.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.",
- "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar."
},
"step": {
@@ -17,9 +18,9 @@
"host": "[%key::common::config_flow::data::host%]",
"password": "[%key::common::config_flow::data::password%]",
"port": "[%key::common::config_flow::data::port%]",
- "ssl": "AdGuard Home utilitza un certificat SSL",
+ "ssl": "Utilitza un certificat SSL",
"username": "[%key::common::config_flow::data::username%]",
- "verify_ssl": "AdGuard Home utilitza un certificat adequat"
+ "verify_ssl": "Verifica el certificat SSL"
},
"description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3."
}
diff --git a/homeassistant/components/adguard/translations/el.json b/homeassistant/components/adguard/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/adguard/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json
index b52d0e5afd8582..e0c1f6d7a0b7f1 100644
--- a/homeassistant/components/adguard/translations/en.json
+++ b/homeassistant/components/adguard/translations/en.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "Updated existing configuration.",
- "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect."
},
"step": {
@@ -17,9 +18,9 @@
"host": "Host",
"password": "Password",
"port": "Port",
- "ssl": "AdGuard Home uses a SSL certificate",
+ "ssl": "Uses an SSL certificate",
"username": "Username",
- "verify_ssl": "AdGuard Home uses a proper certificate"
+ "verify_ssl": "Verify SSL certificate"
},
"description": "Set up your AdGuard Home instance to allow monitoring and control."
}
diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json
index 82ba749dcc8bb5..b6c275a401ccbb 100644
--- a/homeassistant/components/adguard/translations/es.json
+++ b/homeassistant/components/adguard/translations/es.json
@@ -5,6 +5,7 @@
"single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se conect\u00f3."
},
"step": {
diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json
new file mode 100644
index 00000000000000..4acca09cd8e7e8
--- /dev/null
+++ b/homeassistant/components/adguard/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendus eba\u00f5nnestus",
+ "connection_error": "\u00dchenduse loomine nurjus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/fr.json b/homeassistant/components/adguard/translations/fr.json
index bf275f922f1ec9..43cee03560c971 100644
--- a/homeassistant/components/adguard/translations/fr.json
+++ b/homeassistant/components/adguard/translations/fr.json
@@ -5,6 +5,7 @@
"single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "\u00c9chec de connexion."
},
"step": {
diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json
index ac2a903c5e5aca..ff37f64aefcc0a 100644
--- a/homeassistant/components/adguard/translations/it.json
+++ b/homeassistant/components/adguard/translations/it.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "Configurazione esistente aggiornata.",
- "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi."
},
"step": {
@@ -17,9 +18,9 @@
"host": "Host",
"password": "Password",
"port": "Porta",
- "ssl": "AdGuard Home utilizza un certificato SSL",
+ "ssl": "Utilizza un certificato SSL",
"username": "Nome utente",
- "verify_ssl": "AdGuard Home utilizza un certificato appropriato"
+ "verify_ssl": "Verificare il certificato SSL"
},
"description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo."
}
diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json
index abf6df83a6a63e..860abfeb27f077 100644
--- a/homeassistant/components/adguard/translations/lb.json
+++ b/homeassistant/components/adguard/translations/lb.json
@@ -5,6 +5,7 @@
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt."
},
"error": {
+ "cannot_connect": "Feeler beim verbannen",
"connection_error": "Feeler beim verbannen."
},
"step": {
diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json
index a772988c042869..747af5d5610461 100644
--- a/homeassistant/components/adguard/translations/no.json
+++ b/homeassistant/components/adguard/translations/no.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
- "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt."
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Tilkobling mislyktes."
},
"step": {
@@ -17,9 +18,9 @@
"host": "Vert",
"password": "Passord",
"port": "",
- "ssl": "AdGuard Hjem bruker et SSL-sertifikat",
+ "ssl": "Bruker et SSL-sertifikat",
"username": "Brukernavn",
- "verify_ssl": "AdGuard Home bruker et riktig sertifikat"
+ "verify_ssl": "Verifisere SSL-sertifikat"
},
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll."
}
diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json
index ed034fcd1db658..daa700925f6ec3 100644
--- a/homeassistant/components/adguard/translations/pl.json
+++ b/homeassistant/components/adguard/translations/pl.json
@@ -5,7 +5,8 @@
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json
index ed65b1423bdcac..175d0bc1d5a410 100644
--- a/homeassistant/components/adguard/translations/ru.json
+++ b/homeassistant/components/adguard/translations/ru.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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": {
+ "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f."
},
"step": {
@@ -17,9 +18,9 @@
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"username": "\u041b\u043e\u0433\u0438\u043d",
- "verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home."
}
diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json
index 5fbfdbbc59c56b..b883b0a01ba6bb 100644
--- a/homeassistant/components/adguard/translations/zh-Hant.json
+++ b/homeassistant/components/adguard/translations/zh-Hant.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002",
- "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002"
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "\u9023\u7dda\u5931\u6557\u3002"
},
"step": {
@@ -17,9 +18,9 @@
"host": "\u4e3b\u6a5f\u7aef",
"password": "\u5bc6\u78bc",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "AdGuard Home \u4f7f\u7528 SSL \u8a8d\u8b49",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"username": "\u4f7f\u7528\u8005\u540d\u7a31",
- "verify_ssl": "AdGuard Home \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002"
}
diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py
index df8a74dc1d5584..d481420b4d4edd 100644
--- a/homeassistant/components/ads/binary_sensor.py
+++ b/homeassistant/components/ads/binary_sensor.py
@@ -4,6 +4,7 @@
import voluptuous as vol
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOVING,
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
BinarySensorEntity,
@@ -43,7 +44,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity):
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var)
- self._device_class = device_class or "moving"
+ self._device_class = device_class or DEVICE_CLASS_MOVING
async def async_added_to_hass(self):
"""Register device notification."""
diff --git a/homeassistant/components/agent_dvr/strings.json b/homeassistant/components/agent_dvr/strings.json
index 95f992310836f0..476d9956222e9c 100644
--- a/homeassistant/components/agent_dvr/strings.json
+++ b/homeassistant/components/agent_dvr/strings.json
@@ -11,11 +11,11 @@
}
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"device_unavailable": "Device is not available"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/agent_dvr/translations/ca.json b/homeassistant/components/agent_dvr/translations/ca.json
index 401f2166bd1f2d..737692adf75c94 100644
--- a/homeassistant/components/agent_dvr/translations/ca.json
+++ b/homeassistant/components/agent_dvr/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dispositiu ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"device_unavailable": "Dispositiu no est\u00e0 disponible"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/en.json b/homeassistant/components/agent_dvr/translations/en.json
index 0f110a06863401..595dd44522aada 100644
--- a/homeassistant/components/agent_dvr/translations/en.json
+++ b/homeassistant/components/agent_dvr/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"device_unavailable": "Device is not available"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/it.json b/homeassistant/components/agent_dvr/translations/it.json
index 1db719893aa889..1c1e92af95d109 100644
--- a/homeassistant/components/agent_dvr/translations/it.json
+++ b/homeassistant/components/agent_dvr/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"device_unavailable": "Il dispositivo non \u00e8 disponibile"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/no.json b/homeassistant/components/agent_dvr/translations/no.json
index 3fcbb8f161798d..8df278350ca10f 100644
--- a/homeassistant/components/agent_dvr/translations/no.json
+++ b/homeassistant/components/agent_dvr/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"device_unavailable": "Enheten er ikke tilgjengelig"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant/components/agent_dvr/translations/pl.json
index 5045015087f03a..9c101555f78e89 100644
--- a/homeassistant/components/agent_dvr/translations/pl.json
+++ b/homeassistant/components/agent_dvr/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
diff --git a/homeassistant/components/agent_dvr/translations/ru.json b/homeassistant/components/agent_dvr/translations/ru.json
index 69b192fc710ae9..6bda3e2e5912c1 100644
--- a/homeassistant/components/agent_dvr/translations/ru.json
+++ b/homeassistant/components/agent_dvr/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e."
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json
index dba3e0937b7bbd..6c257a415bed68 100644
--- a/homeassistant/components/agent_dvr/translations/zh-Hant.json
+++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528"
},
"step": {
diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py
new file mode 100644
index 00000000000000..4741f8a3b548cd
--- /dev/null
+++ b/homeassistant/components/air_quality/group.py
@@ -0,0 +1,14 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.exclude_domain()
diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py
index 84bad2d3719f3f..8b3b1949ec39f8 100644
--- a/homeassistant/components/airly/config_flow.py
+++ b/homeassistant/components/airly/config_flow.py
@@ -39,7 +39,7 @@ async def async_step_user(self, user_input=None):
self._abort_if_unique_id_configured()
api_key_valid = await self._test_api_key(websession, user_input["api_key"])
if not api_key_valid:
- self._errors["base"] = "auth"
+ self._errors["base"] = "invalid_api_key"
else:
location_valid = await self._test_location(
websession,
diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json
index 8140bc91c5fc60..77de843ffce631 100644
--- a/homeassistant/components/airly/manifest.json
+++ b/homeassistant/components/airly/manifest.json
@@ -3,7 +3,7 @@
"name": "Airly",
"documentation": "https://www.home-assistant.io/integrations/airly",
"codeowners": ["@bieniu"],
- "requirements": ["airly==0.0.2"],
+ "requirements": ["airly==1.0.0"],
"config_flow": true,
"quality_scale": "platinum"
}
diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json
index 8bf7782e910505..9d3352322868cb 100644
--- a/homeassistant/components/airly/strings.json
+++ b/homeassistant/components/airly/strings.json
@@ -7,17 +7,17 @@
"data": {
"name": "Name of the integration",
"api_key": "[%key:common::config_flow::data::api_key%]",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
"error": {
"wrong_location": "No Airly measuring stations in this area.",
- "auth": "API key is not correct."
+ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
},
"abort": {
"already_configured": "Airly integration for these coordinates is already configured."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json
index 9d0854811201de..087b38afb69d22 100644
--- a/homeassistant/components/airly/translations/ca.json
+++ b/homeassistant/components/airly/translations/ca.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "La clau API no \u00e9s correcta.",
+ "invalid_api_key": "Clau API inv\u00e0lida",
"wrong_location": "No hi ha estacions de mesura Airly en aquesta zona."
},
"step": {
diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json
new file mode 100644
index 00000000000000..e39b0aef88d863
--- /dev/null
+++ b/homeassistant/components/airly/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json
index 2dd164823ccfc7..d1faa18aff2c8b 100644
--- a/homeassistant/components/airly/translations/en.json
+++ b/homeassistant/components/airly/translations/en.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "API key is not correct.",
+ "invalid_api_key": "Invalid API key",
"wrong_location": "No Airly measuring stations in this area."
},
"step": {
diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json
index dececf29a6989f..1baf28d12a0867 100644
--- a/homeassistant/components/airly/translations/es.json
+++ b/homeassistant/components/airly/translations/es.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "La clave de la API no es correcta.",
+ "invalid_api_key": "Clave API no v\u00e1lida",
"wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona."
},
"step": {
diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json
new file mode 100644
index 00000000000000..d1a9b20ddb17b9
--- /dev/null
+++ b/homeassistant/components/airly/translations/et.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "invalid_api_key": "Vigane API v\u00f5ti"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json
index ac821ab226cfb8..4259112dbc9d79 100644
--- a/homeassistant/components/airly/translations/fr.json
+++ b/homeassistant/components/airly/translations/fr.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "La cl\u00e9 API n'est pas correcte.",
+ "invalid_api_key": "Cl\u00e9 API invalide",
"wrong_location": "Aucune station de mesure Airly dans cette zone."
},
"step": {
diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json
index e394b7af8d35d6..18a64633d4936a 100644
--- a/homeassistant/components/airly/translations/it.json
+++ b/homeassistant/components/airly/translations/it.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "La chiave API non \u00e8 corretta.",
+ "invalid_api_key": "Chiave API non valida",
"wrong_location": "Nessuna stazione di misurazione Airly in quest'area."
},
"step": {
diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json
index 09e77a311ebb45..47d865e24a61a6 100644
--- a/homeassistant/components/airly/translations/no.json
+++ b/homeassistant/components/airly/translations/no.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "API-n\u00f8kkelen er ikke korrekt.",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel",
"wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det."
},
"step": {
diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json
index 9b3a62331db45b..bda79183980365 100644
--- a/homeassistant/components/airly/translations/ru.json
+++ b/homeassistant/components/airly/translations/ru.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
"wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly."
},
"step": {
diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json
index fc6c58cf97879b..d0b813b84dc661 100644
--- a/homeassistant/components/airly/translations/zh-Hant.json
+++ b/homeassistant/components/airly/translations/zh-Hant.json
@@ -5,6 +5,7 @@
},
"error": {
"auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
"wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002"
},
"step": {
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index f06e4fe70b73de..d6d7a93a3662e1 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -3,7 +3,7 @@
from datetime import timedelta
from math import ceil
-from pyairvisual import Client
+from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
@@ -211,23 +211,22 @@ def _standardize_node_pro_config_entry(hass, config_entry):
async def async_setup_entry(hass, config_entry):
"""Set up AirVisual as config entry."""
- websession = aiohttp_client.async_get_clientsession(hass)
-
if CONF_API_KEY in config_entry.data:
_standardize_geography_config_entry(hass, config_entry)
- client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
+ websession = aiohttp_client.async_get_clientsession(hass)
+ cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession)
async def async_update_data():
"""Get new data from the API."""
if CONF_CITY in config_entry.data:
- api_coro = client.api.city(
+ api_coro = cloud_api.air_quality.city(
config_entry.data[CONF_CITY],
config_entry.data[CONF_STATE],
config_entry.data[CONF_COUNTRY],
)
else:
- api_coro = client.api.nearest_city(
+ api_coro = cloud_api.air_quality.nearest_city(
config_entry.data[CONF_LATITUDE],
config_entry.data[CONF_LONGITUDE],
)
@@ -267,17 +266,13 @@ async def async_update_data():
else:
_standardize_node_pro_config_entry(hass, config_entry)
- client = Client(session=websession)
-
async def async_update_data():
"""Get new data from the API."""
try:
- return await client.node.from_samba(
- config_entry.data[CONF_IP_ADDRESS],
- config_entry.data[CONF_PASSWORD],
- include_history=False,
- include_trends=False,
- )
+ async with NodeSamba(
+ config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]
+ ) as node:
+ return await node.async_get_latest_measurements()
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py
index bb2d64a23db97d..047367fa67c68c 100644
--- a/homeassistant/components/airvisual/air_quality.py
+++ b/homeassistant/components/airvisual/air_quality.py
@@ -40,9 +40,9 @@ def __init__(self, airvisual):
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
- if self.coordinator.data["current"]["settings"]["is_aqi_usa"]:
- return self.coordinator.data["current"]["measurements"]["aqi_us"]
- return self.coordinator.data["current"]["measurements"]["aqi_cn"]
+ if self.coordinator.data["settings"]["is_aqi_usa"]:
+ return self.coordinator.data["measurements"]["aqi_us"]
+ return self.coordinator.data["measurements"]["aqi_cn"]
@property
def available(self):
@@ -52,61 +52,59 @@ def available(self):
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
- return self.coordinator.data["current"]["measurements"].get("co2")
+ return self.coordinator.data["measurements"].get("co2")
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
- "identifiers": {
- (DOMAIN, self.coordinator.data["current"]["serial_number"])
- },
- "name": self.coordinator.data["current"]["settings"]["node_name"],
+ "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
+ "name": self.coordinator.data["settings"]["node_name"],
"manufacturer": "AirVisual",
- "model": f'{self.coordinator.data["current"]["status"]["model"]}',
+ "model": f'{self.coordinator.data["status"]["model"]}',
"sw_version": (
- f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
- f'{self.coordinator.data["current"]["status"]["app_version"]}'
+ f'Version {self.coordinator.data["status"]["system_version"]}'
+ f'{self.coordinator.data["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
- node_name = self.coordinator.data["current"]["settings"]["node_name"]
+ node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: Air Quality"
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
- return self.coordinator.data["current"]["measurements"].get("pm2_5")
+ return self.coordinator.data["measurements"].get("pm2_5")
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
- return self.coordinator.data["current"]["measurements"].get("pm1_0")
+ return self.coordinator.data["measurements"].get("pm1_0")
@property
def particulate_matter_0_1(self):
"""Return the particulate matter 0.1 level."""
- return self.coordinator.data["current"]["measurements"].get("pm0_1")
+ return self.coordinator.data["measurements"].get("pm0_1")
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
- return self.coordinator.data["current"]["serial_number"]
+ return self.coordinator.data["serial_number"]
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
self._attrs.update(
{
- ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"),
+ ATTR_VOC: self.coordinator.data["measurements"].get("voc"),
**{
ATTR_SENSOR_LIFE.format(pollutant): lifespan
- for pollutant, lifespan in self.coordinator.data["current"][
- "status"
- ]["sensor_life"].items()
+ for pollutant, lifespan in self.coordinator.data["status"][
+ "sensor_life"
+ ].items()
},
}
)
diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py
index bb1c262eba76cc..7f8022d31f6aef 100644
--- a/homeassistant/components/airvisual/config_flow.py
+++ b/homeassistant/components/airvisual/config_flow.py
@@ -1,7 +1,7 @@
"""Define a config flow manager for AirVisual."""
import asyncio
-from pyairvisual import Client
+from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import InvalidKeyError, NodeProError
import voluptuous as vol
@@ -108,7 +108,7 @@ async def async_step_geography(self, user_input=None):
return self.async_abort(reason="already_configured")
websession = aiohttp_client.async_get_clientsession(self.hass)
- client = Client(session=websession, api_key=user_input[CONF_API_KEY])
+ cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
# If this is the first (and only the first) time we've seen this API key, check
# that it's valid:
@@ -120,7 +120,7 @@ async def async_step_geography(self, user_input=None):
async with check_keys_lock:
if user_input[CONF_API_KEY] not in checked_keys:
try:
- await client.api.nearest_city()
+ await cloud_api.air_quality.nearest_city()
except InvalidKeyError:
return self.async_show_form(
step_id="geography",
@@ -157,24 +157,20 @@ async def async_step_node_pro(self, user_input=None):
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
- websession = aiohttp_client.async_get_clientsession(self.hass)
- client = Client(session=websession)
+ node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD])
try:
- await client.node.from_samba(
- user_input[CONF_IP_ADDRESS],
- user_input[CONF_PASSWORD],
- include_history=False,
- include_trends=False,
- )
+ await node.async_connect()
except NodeProError as err:
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
return self.async_show_form(
step_id="node_pro",
data_schema=self.node_pro_schema,
- errors={CONF_IP_ADDRESS: "unable_to_connect"},
+ errors={CONF_IP_ADDRESS: "cannot_connect"},
)
+ await node.async_disconnect()
+
return self.async_create_entry(
title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json
index 93b57a4804ee22..d78245512754c8 100644
--- a/homeassistant/components/airvisual/manifest.json
+++ b/homeassistant/components/airvisual/manifest.json
@@ -3,6 +3,6 @@
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
- "requirements": ["pyairvisual==4.4.0"],
+ "requirements": ["pyairvisual==5.0.2"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py
index 895ffa494a44ad..a81c118ecc9480 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -38,10 +38,6 @@
ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region"
-MASS_PARTS_PER_MILLION = "ppm"
-MASS_PARTS_PER_BILLION = "ppb"
-VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
-
SENSOR_KIND_LEVEL = "air_pollution_level"
SENSOR_KIND_AQI = "air_quality_index"
SENSOR_KIND_POLLUTANT = "main_pollutant"
@@ -229,22 +225,20 @@ def device_class(self):
def device_info(self):
"""Return device registry information for this entity."""
return {
- "identifiers": {
- (DOMAIN, self.coordinator.data["current"]["serial_number"])
- },
- "name": self.coordinator.data["current"]["settings"]["node_name"],
+ "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
+ "name": self.coordinator.data["settings"]["node_name"],
"manufacturer": "AirVisual",
- "model": f'{self.coordinator.data["current"]["status"]["model"]}',
+ "model": f'{self.coordinator.data["status"]["model"]}',
"sw_version": (
- f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
- f'{self.coordinator.data["current"]["status"]["app_version"]}'
+ f'Version {self.coordinator.data["status"]["system_version"]}'
+ f'{self.coordinator.data["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
- node_name = self.coordinator.data["current"]["settings"]["node_name"]
+ node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: {self._name}"
@property
@@ -255,18 +249,14 @@ def state(self):
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
- return f"{self.coordinator.data['current']['serial_number']}_{self._kind}"
+ return f"{self.coordinator.data['serial_number']}_{self._kind}"
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
if self._kind == SENSOR_KIND_BATTERY_LEVEL:
- self._state = self.coordinator.data["current"]["status"]["battery"]
+ self._state = self.coordinator.data["status"]["battery"]
elif self._kind == SENSOR_KIND_HUMIDITY:
- self._state = self.coordinator.data["current"]["measurements"].get(
- "humidity"
- )
+ self._state = self.coordinator.data["measurements"].get("humidity")
elif self._kind == SENSOR_KIND_TEMPERATURE:
- self._state = self.coordinator.data["current"]["measurements"].get(
- "temperature_C"
- )
+ self._state = self.coordinator.data["measurements"].get("temperature_C")
diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json
index c7e2bc347016a2..fd8e10f105e9fd 100644
--- a/homeassistant/components/airvisual/strings.json
+++ b/homeassistant/components/airvisual/strings.json
@@ -6,8 +6,8 @@
"description": "Use the AirVisual cloud API to monitor a geographical location.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
},
"node_pro": {
@@ -29,9 +29,9 @@
}
},
"error": {
- "general_error": "There was an unknown error.",
- "invalid_api_key": "Invalid API key provided.",
- "unable_to_connect": "Unable to connect to Node/Pro unit."
+ "general_error": "[%key:common::config_flow::error::unknown%]",
+ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "These coordinates or Node/Pro ID are already registered."
@@ -47,4 +47,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json
index 045be812dc2031..98e09971ae341c 100644
--- a/homeassistant/components/airvisual/translations/ca.json
+++ b/homeassistant/components/airvisual/translations/ca.json
@@ -4,8 +4,9 @@
"already_configured": "Aquestes coordenades o Node/Pro ID ja estan registrades."
},
"error": {
- "general_error": "S'ha produ\u00eft un error desconegut.",
- "invalid_api_key": "Clau API proporiconada no v\u00e0lida.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "general_error": "Error inesperat",
+ "invalid_api_key": "Clau API inv\u00e0lida",
"unable_to_connect": "No s'ha pogut connectar a la unitat Node/Pro."
},
"step": {
diff --git a/homeassistant/components/airvisual/translations/el.json b/homeassistant/components/airvisual/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json
index a85c2ba75cef39..beb7ebe6bf3b02 100644
--- a/homeassistant/components/airvisual/translations/en.json
+++ b/homeassistant/components/airvisual/translations/en.json
@@ -4,8 +4,9 @@
"already_configured": "These coordinates or Node/Pro ID are already registered."
},
"error": {
- "general_error": "There was an unknown error.",
- "invalid_api_key": "Invalid API key provided.",
+ "cannot_connect": "Failed to connect",
+ "general_error": "Unexpected error",
+ "invalid_api_key": "Invalid API key",
"unable_to_connect": "Unable to connect to Node/Pro unit."
},
"step": {
diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json
index dbb44ed4abe473..2ef588f3accf04 100644
--- a/homeassistant/components/airvisual/translations/es.json
+++ b/homeassistant/components/airvisual/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "Estas coordenadas o Nodo/Pro ID ya est\u00e1n registradas."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"general_error": "Se ha producido un error desconocido.",
"invalid_api_key": "Se proporciona una clave API no v\u00e1lida.",
"unable_to_connect": "No se puede conectar a la unidad Node/Pro."
diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json
new file mode 100644
index 00000000000000..eb795276597901
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "geography": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ }
+ },
+ "user": {
+ "data": {
+ "cloud_api": "Geograafiline asukoht"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json
index 7bd42083e6dcf0..55247360306fe5 100644
--- a/homeassistant/components/airvisual/translations/fr.json
+++ b/homeassistant/components/airvisual/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"general_error": "Une erreur inconnue est survenue.",
"invalid_api_key": "La cl\u00e9 API fournie n'est pas valide.",
"unable_to_connect": "Impossible de se connecter \u00e0 l'unit\u00e9 Node / Pro."
diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json
index c22481918a0eb3..1edb269d37427a 100644
--- a/homeassistant/components/airvisual/translations/it.json
+++ b/homeassistant/components/airvisual/translations/it.json
@@ -4,8 +4,9 @@
"already_configured": "Queste coordinate o Node/Pro ID sono gi\u00e0 registrate."
},
"error": {
- "general_error": "Si \u00e8 verificato un errore sconosciuto.",
- "invalid_api_key": "Chiave API non valida fornita.",
+ "cannot_connect": "Impossibile connettersi",
+ "general_error": "Errore imprevisto",
+ "invalid_api_key": "Chiave API non valida",
"unable_to_connect": "Impossibile connettersi all'unit\u00e0 Node/Pro."
},
"step": {
diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json
index d0cffc8ec2d309..cca720da82c030 100644
--- a/homeassistant/components/airvisual/translations/lb.json
+++ b/homeassistant/components/airvisual/translations/lb.json
@@ -4,6 +4,7 @@
"already_configured": "D\u00ebs Koordinate oder ode/Pro ID si schon registr\u00e9iert."
},
"error": {
+ "cannot_connect": "Feeler beim verbannen",
"general_error": "Onbekannten Feeler",
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel uginn",
"unable_to_connect": "Kann sech net mat der Node/Pri verbannen."
diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json
index 8fcf00a6714b84..80e0f6631c6c96 100644
--- a/homeassistant/components/airvisual/translations/no.json
+++ b/homeassistant/components/airvisual/translations/no.json
@@ -4,8 +4,9 @@
"already_configured": "Disse koordinatene eller Node / Pro ID er allerede registrert."
},
"error": {
- "general_error": "Det oppstod en ukjent feil.",
- "invalid_api_key": "Ugyldig API-n\u00f8kkel angitt.",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "general_error": "Uventet feil",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel",
"unable_to_connect": "Kan ikke koble til Node / Pro-enheten."
},
"step": {
diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json
index dea77a233aabb8..b00a29a8a5da01 100644
--- a/homeassistant/components/airvisual/translations/pl.json
+++ b/homeassistant/components/airvisual/translations/pl.json
@@ -4,7 +4,8 @@
"already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu."
},
"error": {
- "general_error": "Nieoczekiwany b\u0142\u0105d.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "general_error": "Nieoczekiwany b\u0142\u0105d",
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
"unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z jednostk\u0105 Node/Pro."
},
diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json
index 67af449c9b0e46..c35aefa3b9375c 100644
--- a/homeassistant/components/airvisual/translations/ru.json
+++ b/homeassistant/components/airvisual/translations/ru.json
@@ -4,7 +4,8 @@
"already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e."
},
"error": {
- "general_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
"unable_to_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json
index 80e8372f3f6d51..689b95027fa93c 100644
--- a/homeassistant/components/airvisual/translations/zh-Hant.json
+++ b/homeassistant/components/airvisual/translations/zh-Hant.json
@@ -4,8 +4,9 @@
"already_configured": "\u6b64\u5ea7\u6a19\u6216 Node/Pro ID \u5df2\u8a3b\u518a\u3002"
},
"error": {
- "general_error": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002",
- "invalid_api_key": "API \u5bc6\u9470\u7121\u6548\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "general_error": "\u672a\u9810\u671f\u932f\u8aa4",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
"unable_to_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Node/Pro \u8a2d\u5099\u3002"
},
"step": {
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index 50b8adb4c032de..114abfa9cd6e4c 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -175,12 +175,11 @@ def supported_features(self) -> int:
@property
def state_attributes(self):
"""Return the state attributes."""
- state_attr = {
+ return {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
ATTR_CODE_ARM_REQUIRED: self.code_arm_required,
}
- return state_attr
class AlarmControlPanel(AlarmControlPanelEntity):
diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py
index 81e444ae16f0cb..0dc16fdcf42275 100644
--- a/homeassistant/components/alarm_control_panel/device_action.py
+++ b/homeassistant/components/alarm_control_panel/device_action.py
@@ -6,6 +6,7 @@
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_CODE,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -56,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
if state is None:
continue
- supported_features = state.attributes["supported_features"]
+ supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
# Add actions for each entity that belongs to this integration
if supported_features & SUPPORT_ALARM_ARM_AWAY:
diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py
index c4d43d1b051472..e5b3ec6aeee960 100644
--- a/homeassistant/components/alarm_control_panel/device_condition.py
+++ b/homeassistant/components/alarm_control_panel/device_condition.py
@@ -11,6 +11,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -73,7 +74,7 @@ async def async_get_conditions(
if state is None:
continue
- supported_features = state.attributes["supported_features"]
+ supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
# Add conditions for each entity that belongs to this integration
conditions += [
diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py
index eeea1dbbf33a31..cb07ff35e962c2 100644
--- a/homeassistant/components/alarm_control_panel/device_trigger.py
+++ b/homeassistant/components/alarm_control_panel/device_trigger.py
@@ -12,6 +12,7 @@
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
+ ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -64,7 +65,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
if entity_state is None:
continue
- supported_features = entity_state.attributes["supported_features"]
+ supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES]
# Add triggers for each entity that belongs to this integration
triggers += [
diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py
new file mode 100644
index 00000000000000..6645f12245d257
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/group.py
@@ -0,0 +1,31 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_TRIGGERED,
+ STATE_OFF,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states(
+ {
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_TRIGGERED,
+ },
+ STATE_OFF,
+ )
diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json
index 28c47b5a06d973..76b0c845d01247 100644
--- a/homeassistant/components/alarm_control_panel/translations/et.json
+++ b/homeassistant/components/alarm_control_panel/translations/et.json
@@ -1,4 +1,27 @@
{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Valvesta {entity_name}",
+ "arm_home": "Valvesta {entity_name} kodus re\u017eiimis",
+ "arm_night": "Valvesta {entity_name} \u00f6\u00f6re\u017eiimis",
+ "disarm": "V\u00f5ta {entity_name} valvest maha",
+ "trigger": "K\u00e4ivita {entity_name}"
+ },
+ "condition_type": {
+ "is_armed_away": "{entity_name} on valvestatud",
+ "is_armed_home": "{entity_name} on valvestatud kodure\u017eiimis",
+ "is_armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis",
+ "is_disarmed": "{entity_name} on valve alt maas",
+ "is_triggered": "{entity_name} on h\u00e4iret andnud"
+ },
+ "trigger_type": {
+ "armed_away": "{entity_name} valvestatus",
+ "armed_home": "{entity_name} valvestatus kodure\u017eiimis",
+ "armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis",
+ "disarmed": "{entity_name} v\u00f5eti valvest maha",
+ "triggered": "{entity_name} andis h\u00e4iret"
+ }
+ },
"state": {
"_": {
"armed": "Valves",
diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json
index 15b5fd8457c2ef..0a0f33d6181f6d 100644
--- a/homeassistant/components/alarm_control_panel/translations/nl.json
+++ b/homeassistant/components/alarm_control_panel/translations/nl.json
@@ -25,7 +25,7 @@
"state": {
"_": {
"armed": "Ingeschakeld",
- "armed_away": "Afwezig Ingeschakeld",
+ "armed_away": "Ingeschakeld afwezig",
"armed_custom_bypass": "Ingeschakeld met overbrugging(en)",
"armed_home": "Ingeschakeld thuis",
"armed_night": "Ingeschakeld nacht",
diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py
index 0aa9fcc29eca49..8dd704f133345a 100644
--- a/homeassistant/components/alarmdecoder/__init__.py
+++ b/homeassistant/components/alarmdecoder/__init__.py
@@ -1,167 +1,82 @@
"""Support for AlarmDecoder devices."""
+import asyncio
from datetime import timedelta
import logging
from adext import AdExt
-from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
+from alarmdecoder.devices import SerialDevice, SocketDevice
from alarmdecoder.util import NoDeviceError
-import voluptuous as vol
-from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
-from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import load_platform
-from homeassistant.util import dt as dt_util
-
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = "alarmdecoder"
-
-DATA_AD = "alarmdecoder"
-
-CONF_DEVICE = "device"
-CONF_DEVICE_BAUD = "baudrate"
-CONF_DEVICE_PATH = "path"
-CONF_DEVICE_PORT = "port"
-CONF_DEVICE_TYPE = "type"
-CONF_AUTO_BYPASS = "autobypass"
-CONF_PANEL_DISPLAY = "panel_display"
-CONF_ZONE_NAME = "name"
-CONF_ZONE_TYPE = "type"
-CONF_ZONE_LOOP = "loop"
-CONF_ZONE_RFID = "rfid"
-CONF_ZONES = "zones"
-CONF_RELAY_ADDR = "relayaddr"
-CONF_RELAY_CHAN = "relaychan"
-CONF_CODE_ARM_REQUIRED = "code_arm_required"
-
-DEFAULT_DEVICE_TYPE = "socket"
-DEFAULT_DEVICE_HOST = "localhost"
-DEFAULT_DEVICE_PORT = 10000
-DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
-DEFAULT_DEVICE_BAUD = 115200
-
-DEFAULT_AUTO_BYPASS = False
-DEFAULT_PANEL_DISPLAY = False
-DEFAULT_CODE_ARM_REQUIRED = True
-
-DEFAULT_ZONE_TYPE = "opening"
-
-SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
-SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away"
-SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home"
-SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm"
-
-SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
-SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
-SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
-SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
-
-DEVICE_SOCKET_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_DEVICE_TYPE): "socket",
- vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
- vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port,
- }
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PORT,
+ CONF_PROTOCOL,
+ EVENT_HOMEASSISTANT_STOP,
)
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util import dt as dt_util
-DEVICE_SERIAL_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_DEVICE_TYPE): "serial",
- vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
- vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string,
- }
+from .const import (
+ CONF_DEVICE_BAUD,
+ CONF_DEVICE_PATH,
+ DATA_AD,
+ DATA_REMOVE_STOP_LISTENER,
+ DATA_REMOVE_UPDATE_LISTENER,
+ DATA_RESTART,
+ DOMAIN,
+ PROTOCOL_SERIAL,
+ PROTOCOL_SOCKET,
+ SIGNAL_PANEL_MESSAGE,
+ SIGNAL_REL_MESSAGE,
+ SIGNAL_RFX_MESSAGE,
+ SIGNAL_ZONE_FAULT,
+ SIGNAL_ZONE_RESTORE,
)
-DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"})
-
-ZONE_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_ZONE_NAME): cv.string,
- vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(
- DEVICE_CLASSES_SCHEMA
- ),
- vol.Optional(CONF_ZONE_RFID): cv.string,
- vol.Optional(CONF_ZONE_LOOP): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
- vol.Inclusive(
- CONF_RELAY_ADDR,
- "relaylocation",
- "Relay address and channel must exist together",
- ): cv.byte,
- vol.Inclusive(
- CONF_RELAY_CHAN,
- "relaylocation",
- "Relay address and channel must exist together",
- ): cv.byte,
- }
-)
+_LOGGER = logging.getLogger(__name__)
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_DEVICE): vol.Any(
- DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA
- ),
- vol.Optional(
- CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY
- ): cv.boolean,
- vol.Optional(CONF_AUTO_BYPASS, default=DEFAULT_AUTO_BYPASS): cv.boolean,
- vol.Optional(
- CONF_CODE_ARM_REQUIRED, default=DEFAULT_CODE_ARM_REQUIRED
- ): cv.boolean,
- vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
+PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"]
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up for the AlarmDecoder devices."""
- conf = config.get(DOMAIN)
+ return True
+
- restart = False
- device = conf[CONF_DEVICE]
- display = conf[CONF_PANEL_DISPLAY]
- auto_bypass = conf[CONF_AUTO_BYPASS]
- code_arm_required = conf[CONF_CODE_ARM_REQUIRED]
- zones = conf.get(CONF_ZONES)
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up AlarmDecoder config flow."""
+ undo_listener = entry.add_update_listener(_update_listener)
- device_type = device[CONF_DEVICE_TYPE]
- host = DEFAULT_DEVICE_HOST
- port = DEFAULT_DEVICE_PORT
- path = DEFAULT_DEVICE_PATH
- baud = DEFAULT_DEVICE_BAUD
+ ad_connection = entry.data
+ protocol = ad_connection[CONF_PROTOCOL]
def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder."""
+ if not hass.data.get(DOMAIN):
+ return
_LOGGER.debug("Shutting down alarmdecoder")
- nonlocal restart
- restart = False
+ hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
controller.close()
- def open_connection(now=None):
+ async def open_connection(now=None):
"""Open a connection to AlarmDecoder."""
- nonlocal restart
try:
- controller.open(baud)
+ await hass.async_add_executor_job(controller.open, baud)
except NoDeviceError:
- _LOGGER.debug("Failed to connect. Retrying in 5 seconds")
- hass.helpers.event.track_point_in_time(
+ _LOGGER.debug("Failed to connect. Retrying in 5 seconds")
+ hass.helpers.event.async_track_point_in_time(
open_connection, dt_util.utcnow() + timedelta(seconds=5)
)
return
_LOGGER.debug("Established a connection with the alarmdecoder")
- restart = True
+ hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True
def handle_closed_connection(event):
"""Restart after unexpected loss of connection."""
- nonlocal restart
- if not restart:
+ if not hass.data[DOMAIN][entry.entry_id][DATA_RESTART]:
return
- restart = False
+ hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
_LOGGER.warning("AlarmDecoder unexpectedly lost connection")
hass.add_job(open_connection)
@@ -185,18 +100,14 @@ def handle_rel_message(sender, message):
"""Handle relay or zone expander message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
- controller = False
- if device_type == "socket":
- host = device[CONF_HOST]
- port = device[CONF_DEVICE_PORT]
+ baud = ad_connection.get(CONF_DEVICE_BAUD)
+ if protocol == PROTOCOL_SOCKET:
+ host = ad_connection[CONF_HOST]
+ port = ad_connection[CONF_PORT]
controller = AdExt(SocketDevice(interface=(host, port)))
- elif device_type == "serial":
- path = device[CONF_DEVICE_PATH]
- baud = device[CONF_DEVICE_BAUD]
+ if protocol == PROTOCOL_SERIAL:
+ path = ad_connection[CONF_DEVICE_PATH]
controller = AdExt(SerialDevice(interface=path))
- elif device_type == "usb":
- AdExt(USBDevice.find())
- return False
controller.on_message += handle_message
controller.on_rfx_message += handle_rfx_message
@@ -205,24 +116,56 @@ def handle_rel_message(sender, message):
controller.on_close += handle_closed_connection
controller.on_expander_message += handle_rel_message
- hass.data[DATA_AD] = controller
+ remove_stop_listener = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder
+ )
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_AD: controller,
+ DATA_REMOVE_UPDATE_LISTENER: undo_listener,
+ DATA_REMOVE_STOP_LISTENER: remove_stop_listener,
+ DATA_RESTART: False,
+ }
+
+ await open_connection()
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+ return True
- open_connection()
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Unload a AlarmDecoder entry."""
+ hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
- load_platform(
- hass,
- "alarm_control_panel",
- DOMAIN,
- {CONF_AUTO_BYPASS: auto_bypass, CONF_CODE_ARM_REQUIRED: code_arm_required},
- config,
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
)
- if zones:
- load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
+ if not unload_ok:
+ return False
- if display:
- load_platform(hass, "sensor", DOMAIN, conf, config)
+ hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_UPDATE_LISTENER]()
+ hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_STOP_LISTENER]()
+ await hass.async_add_executor_job(hass.data[DOMAIN][entry.entry_id][DATA_AD].close)
+
+ if hass.data[DOMAIN][entry.entry_id]:
+ hass.data[DOMAIN].pop(entry.entry_id)
+ if not hass.data[DOMAIN]:
+ hass.data.pop(DOMAIN)
return True
+
+
+async def _update_listener(hass: HomeAssistantType, entry: ConfigEntry):
+ """Handle options update."""
+ _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"])
+ await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py
index 117374552f3b66..bc2d74a5042619 100644
--- a/homeassistant/components/alarmdecoder/alarm_control_panel.py
+++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py
@@ -12,6 +12,7 @@
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
@@ -20,66 +21,70 @@
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
+from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
-from . import (
+from .const import (
+ CONF_ALT_NIGHT_MODE,
CONF_AUTO_BYPASS,
CONF_CODE_ARM_REQUIRED,
DATA_AD,
+ DEFAULT_ARM_OPTIONS,
DOMAIN,
+ OPTIONS_ARM,
SIGNAL_PANEL_MESSAGE,
)
_LOGGER = logging.getLogger(__name__)
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
-ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
-ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string})
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+):
"""Set up for AlarmDecoder alarm panels."""
- if discovery_info is None:
- return
-
- auto_bypass = discovery_info[CONF_AUTO_BYPASS]
- code_arm_required = discovery_info[CONF_CODE_ARM_REQUIRED]
- entity = AlarmDecoderAlarmPanel(auto_bypass, code_arm_required)
- add_entities([entity])
+ options = entry.options
+ arm_options = options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
+ client = hass.data[DOMAIN][entry.entry_id][DATA_AD]
+
+ entity = AlarmDecoderAlarmPanel(
+ client=client,
+ auto_bypass=arm_options[CONF_AUTO_BYPASS],
+ code_arm_required=arm_options[CONF_CODE_ARM_REQUIRED],
+ alt_night_mode=arm_options[CONF_ALT_NIGHT_MODE],
+ )
+ async_add_entities([entity])
- def alarm_toggle_chime_handler(service):
- """Register toggle chime handler."""
- code = service.data.get(ATTR_CODE)
- entity.alarm_toggle_chime(code)
+ platform = entity_platform.current_platform.get()
- hass.services.register(
- DOMAIN,
+ platform.async_register_entity_service(
SERVICE_ALARM_TOGGLE_CHIME,
- alarm_toggle_chime_handler,
- schema=ALARM_TOGGLE_CHIME_SCHEMA,
+ {
+ vol.Required(ATTR_CODE): cv.string,
+ },
+ "alarm_toggle_chime",
)
- def alarm_keypress_handler(service):
- """Register keypress handler."""
- keypress = service.data[ATTR_KEYPRESS]
- entity.alarm_keypress(keypress)
-
- hass.services.register(
- DOMAIN,
+ platform.async_register_entity_service(
SERVICE_ALARM_KEYPRESS,
- alarm_keypress_handler,
- schema=ALARM_KEYPRESS_SCHEMA,
+ {
+ vol.Required(ATTR_KEYPRESS): cv.string,
+ },
+ "alarm_keypress",
)
class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
"""Representation of an AlarmDecoder-based alarm panel."""
- def __init__(self, auto_bypass, code_arm_required):
+ def __init__(self, client, auto_bypass, code_arm_required, alt_night_mode):
"""Initialize the alarm panel."""
+ self._client = client
self._display = ""
self._name = "Alarm Panel"
self._state = None
@@ -95,6 +100,7 @@ def __init__(self, auto_bypass, code_arm_required):
self._zone_bypassed = None
self._auto_bypass = auto_bypass
self._code_arm_required = code_arm_required
+ self._alt_night_mode = alt_night_mode
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -180,11 +186,11 @@ def device_state_attributes(self):
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code:
- self.hass.data[DATA_AD].send(f"{code!s}1")
+ self._client.send(f"{code!s}1")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
- self.hass.data[DATA_AD].arm_away(
+ self._client.arm_away(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
@@ -192,7 +198,7 @@ def alarm_arm_away(self, code=None):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
- self.hass.data[DATA_AD].arm_home(
+ self._client.arm_home(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
@@ -200,18 +206,19 @@ def alarm_arm_home(self, code=None):
def alarm_arm_night(self, code=None):
"""Send arm night command."""
- self.hass.data[DATA_AD].arm_night(
+ self._client.arm_night(
code=code,
code_arm_required=self._code_arm_required,
+ alt_night_mode=self._alt_night_mode,
auto_bypass=self._auto_bypass,
)
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""
if code:
- self.hass.data[DATA_AD].send(f"{code!s}9")
+ self._client.send(f"{code!s}9")
def alarm_keypress(self, keypress):
"""Send custom keypresses."""
if keypress:
- self.hass.data[DATA_AD].send(keypress)
+ self._client.send(keypress)
diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py
index cec1b8356b0234..55bf13d7fef0b5 100644
--- a/homeassistant/components/alarmdecoder/binary_sensor.py
+++ b/homeassistant/components/alarmdecoder/binary_sensor.py
@@ -2,20 +2,23 @@
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
-from . import (
+from .const import (
CONF_RELAY_ADDR,
CONF_RELAY_CHAN,
CONF_ZONE_LOOP,
CONF_ZONE_NAME,
+ CONF_ZONE_NUMBER,
CONF_ZONE_RFID,
CONF_ZONE_TYPE,
- CONF_ZONES,
+ DEFAULT_ZONE_OPTIONS,
+ OPTIONS_ZONES,
SIGNAL_REL_MESSAGE,
SIGNAL_RFX_MESSAGE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE,
- ZONE_SCHEMA,
)
_LOGGER = logging.getLogger(__name__)
@@ -30,27 +33,28 @@
ATTR_RF_LOOP1 = "rf_loop1"
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the AlarmDecoder binary sensor devices."""
- configured_zones = discovery_info[CONF_ZONES]
-
- devices = []
- for zone_num in configured_zones:
- device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
- zone_type = device_config_data[CONF_ZONE_TYPE]
- zone_name = device_config_data[CONF_ZONE_NAME]
- zone_rfid = device_config_data.get(CONF_ZONE_RFID)
- zone_loop = device_config_data.get(CONF_ZONE_LOOP)
- relay_addr = device_config_data.get(CONF_RELAY_ADDR)
- relay_chan = device_config_data.get(CONF_RELAY_CHAN)
- device = AlarmDecoderBinarySensor(
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+):
+ """Set up for AlarmDecoder sensor."""
+
+ zones = entry.options.get(OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS)
+
+ entities = []
+ for zone_num in zones:
+ zone_info = zones[zone_num]
+ zone_type = zone_info[CONF_ZONE_TYPE]
+ zone_name = zone_info[CONF_ZONE_NAME]
+ zone_rfid = zone_info.get(CONF_ZONE_RFID)
+ zone_loop = zone_info.get(CONF_ZONE_LOOP)
+ relay_addr = zone_info.get(CONF_RELAY_ADDR)
+ relay_chan = zone_info.get(CONF_RELAY_CHAN)
+ entity = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan
)
- devices.append(device)
-
- add_entities(devices)
+ entities.append(entity)
- return True
+ async_add_entities(entities)
class AlarmDecoderBinarySensor(BinarySensorEntity):
@@ -67,7 +71,7 @@ def __init__(
relay_chan,
):
"""Initialize the binary_sensor."""
- self._zone_number = zone_number
+ self._zone_number = int(zone_number)
self._zone_type = zone_type
self._state = None
self._name = zone_name
@@ -116,7 +120,7 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
+ attr = {CONF_ZONE_NUMBER: self._zone_number}
if self._rfid and self._rfstate is not None:
attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01)
attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02)
diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py
new file mode 100644
index 00000000000000..74b23f049a710b
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/config_flow.py
@@ -0,0 +1,360 @@
+"""Config flow for AlarmDecoder."""
+import logging
+
+from adext import AdExt
+from alarmdecoder.devices import SerialDevice, SocketDevice
+from alarmdecoder.util import NoDeviceError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.binary_sensor import DEVICE_CLASSES
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
+from homeassistant.core import callback
+
+from .const import ( # pylint: disable=unused-import
+ CONF_ALT_NIGHT_MODE,
+ CONF_AUTO_BYPASS,
+ CONF_CODE_ARM_REQUIRED,
+ CONF_DEVICE_BAUD,
+ CONF_DEVICE_PATH,
+ CONF_RELAY_ADDR,
+ CONF_RELAY_CHAN,
+ CONF_ZONE_LOOP,
+ CONF_ZONE_NAME,
+ CONF_ZONE_NUMBER,
+ CONF_ZONE_RFID,
+ CONF_ZONE_TYPE,
+ DEFAULT_ARM_OPTIONS,
+ DEFAULT_DEVICE_BAUD,
+ DEFAULT_DEVICE_HOST,
+ DEFAULT_DEVICE_PATH,
+ DEFAULT_DEVICE_PORT,
+ DEFAULT_ZONE_OPTIONS,
+ DEFAULT_ZONE_TYPE,
+ DOMAIN,
+ OPTIONS_ARM,
+ OPTIONS_ZONES,
+ PROTOCOL_SERIAL,
+ PROTOCOL_SOCKET,
+)
+
+EDIT_KEY = "edit_selection"
+EDIT_ZONES = "Zones"
+EDIT_SETTINGS = "Arming Settings"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a AlarmDecoder config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ def __init__(self):
+ """Initialize AlarmDecoder ConfigFlow."""
+ self.protocol = None
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for AlarmDecoder."""
+ return AlarmDecoderOptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if user_input is not None:
+ self.protocol = user_input[CONF_PROTOCOL]
+ return await self.async_step_protocol()
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_PROTOCOL): vol.In(
+ [PROTOCOL_SOCKET, PROTOCOL_SERIAL]
+ ),
+ }
+ ),
+ )
+
+ async def async_step_protocol(self, user_input=None):
+ """Handle AlarmDecoder protocol setup."""
+ errors = {}
+ if user_input is not None:
+ if _device_already_added(
+ self._async_current_entries(), user_input, self.protocol
+ ):
+ return self.async_abort(reason="already_configured")
+ connection = {}
+ baud = None
+ if self.protocol == PROTOCOL_SOCKET:
+ host = connection[CONF_HOST] = user_input[CONF_HOST]
+ port = connection[CONF_PORT] = user_input[CONF_PORT]
+ title = f"{host}:{port}"
+ device = SocketDevice(interface=(host, port))
+ if self.protocol == PROTOCOL_SERIAL:
+ path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
+ baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
+ title = path
+ device = SerialDevice(interface=path)
+
+ controller = AdExt(device)
+
+ def test_connection():
+ controller.open(baud)
+ controller.close()
+
+ try:
+ await self.hass.async_add_executor_job(test_connection)
+ return self.async_create_entry(
+ title=title, data={CONF_PROTOCOL: self.protocol, **connection}
+ )
+ except NoDeviceError:
+ errors["base"] = "service_unavailable"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception during AlarmDecoder setup")
+ errors["base"] = "unknown"
+
+ if self.protocol == PROTOCOL_SOCKET:
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str,
+ vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int,
+ }
+ )
+ if self.protocol == PROTOCOL_SERIAL:
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str,
+ vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int,
+ }
+ )
+
+ return self.async_show_form(
+ step_id="protocol",
+ data_schema=schema,
+ errors=errors,
+ )
+
+
+class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle AlarmDecoder options."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Initialize AlarmDecoder options flow."""
+ self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
+ self.zone_options = config_entry.options.get(
+ OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
+ )
+ self.selected_zone = None
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ if user_input is not None:
+ if user_input[EDIT_KEY] == EDIT_SETTINGS:
+ return await self.async_step_arm_settings()
+ if user_input[EDIT_KEY] == EDIT_ZONES:
+ return await self.async_step_zone_select()
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In(
+ [EDIT_SETTINGS, EDIT_ZONES]
+ )
+ },
+ ),
+ )
+
+ async def async_step_arm_settings(self, user_input=None):
+ """Arming options form."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title="",
+ data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_options},
+ )
+
+ return self.async_show_form(
+ step_id="arm_settings",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_ALT_NIGHT_MODE,
+ default=self.arm_options[CONF_ALT_NIGHT_MODE],
+ ): bool,
+ vol.Optional(
+ CONF_AUTO_BYPASS, default=self.arm_options[CONF_AUTO_BYPASS]
+ ): bool,
+ vol.Optional(
+ CONF_CODE_ARM_REQUIRED,
+ default=self.arm_options[CONF_CODE_ARM_REQUIRED],
+ ): bool,
+ },
+ ),
+ )
+
+ async def async_step_zone_select(self, user_input=None):
+ """Zone selection form."""
+ errors = _validate_zone_input(user_input)
+
+ if user_input is not None and not errors:
+ self.selected_zone = str(
+ int(user_input[CONF_ZONE_NUMBER])
+ ) # remove leading zeros
+ return await self.async_step_zone_details()
+
+ return self.async_show_form(
+ step_id="zone_select",
+ data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}),
+ errors=errors,
+ )
+
+ async def async_step_zone_details(self, user_input=None):
+ """Zone details form."""
+ errors = _validate_zone_input(user_input)
+
+ if user_input is not None and not errors:
+ zone_options = self.zone_options.copy()
+ zone_id = self.selected_zone
+ zone_options[zone_id] = _fix_input_types(user_input)
+
+ # Delete zone entry if zone_name is omitted
+ if CONF_ZONE_NAME not in zone_options[zone_id]:
+ zone_options.pop(zone_id)
+
+ return self.async_create_entry(
+ title="",
+ data={OPTIONS_ARM: self.arm_options, OPTIONS_ZONES: zone_options},
+ )
+
+ existing_zone_settings = self.zone_options.get(self.selected_zone, {})
+
+ return self.async_show_form(
+ step_id="zone_details",
+ description_placeholders={CONF_ZONE_NUMBER: self.selected_zone},
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_ZONE_NAME,
+ description={
+ "suggested_value": existing_zone_settings.get(
+ CONF_ZONE_NAME
+ )
+ },
+ ): str,
+ vol.Optional(
+ CONF_ZONE_TYPE,
+ default=existing_zone_settings.get(
+ CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE
+ ),
+ ): vol.In(DEVICE_CLASSES),
+ vol.Optional(
+ CONF_ZONE_RFID,
+ description={
+ "suggested_value": existing_zone_settings.get(
+ CONF_ZONE_RFID
+ )
+ },
+ ): str,
+ vol.Optional(
+ CONF_ZONE_LOOP,
+ description={
+ "suggested_value": existing_zone_settings.get(
+ CONF_ZONE_LOOP
+ )
+ },
+ ): str,
+ vol.Optional(
+ CONF_RELAY_ADDR,
+ description={
+ "suggested_value": existing_zone_settings.get(
+ CONF_RELAY_ADDR
+ )
+ },
+ ): str,
+ vol.Optional(
+ CONF_RELAY_CHAN,
+ description={
+ "suggested_value": existing_zone_settings.get(
+ CONF_RELAY_CHAN
+ )
+ },
+ ): str,
+ }
+ ),
+ errors=errors,
+ )
+
+
+def _validate_zone_input(zone_input):
+ if not zone_input:
+ return {}
+ errors = {}
+
+ # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive
+ if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or (
+ CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input
+ ):
+ errors["base"] = "relay_inclusive"
+
+ # The following keys must be int
+ for key in [CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]:
+ if key in zone_input:
+ try:
+ int(zone_input[key])
+ except ValueError:
+ errors[key] = "int"
+
+ # CONF_ZONE_LOOP depends on CONF_ZONE_RFID
+ if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input:
+ errors[CONF_ZONE_LOOP] = "loop_rfid"
+
+ # CONF_ZONE_LOOP must be 1-4
+ if (
+ CONF_ZONE_LOOP in zone_input
+ and zone_input[CONF_ZONE_LOOP].isdigit()
+ and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5))
+ ):
+ errors[CONF_ZONE_LOOP] = "loop_range"
+
+ return errors
+
+
+def _fix_input_types(zone_input):
+ """Convert necessary keys to int.
+
+ Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
+ strings and then convert them to ints.
+ """
+
+ for key in [CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]:
+ if key in zone_input:
+ zone_input[key] = int(zone_input[key])
+
+ return zone_input
+
+
+def _device_already_added(current_entries, user_input, protocol):
+ """Determine if entry has already been added to HA."""
+ user_host = user_input.get(CONF_HOST)
+ user_port = user_input.get(CONF_PORT)
+ user_path = user_input.get(CONF_DEVICE_PATH)
+ user_baud = user_input.get(CONF_DEVICE_BAUD)
+
+ for entry in current_entries:
+ entry_host = entry.data.get(CONF_HOST)
+ entry_port = entry.data.get(CONF_PORT)
+ entry_path = entry.data.get(CONF_DEVICE_PATH)
+ entry_baud = entry.data.get(CONF_DEVICE_BAUD)
+
+ if protocol == PROTOCOL_SOCKET:
+ if user_host == entry_host and user_port == entry_port:
+ return True
+
+ if protocol == PROTOCOL_SERIAL:
+ if user_baud == entry_baud and user_path == entry_path:
+ return True
+
+ return False
diff --git a/homeassistant/components/alarmdecoder/const.py b/homeassistant/components/alarmdecoder/const.py
new file mode 100644
index 00000000000000..f1bfb66f0d468f
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/const.py
@@ -0,0 +1,49 @@
+"""Constants for the AlarmDecoder component."""
+
+CONF_ALT_NIGHT_MODE = "alt_night_mode"
+CONF_AUTO_BYPASS = "auto_bypass"
+CONF_CODE_ARM_REQUIRED = "code_arm_required"
+CONF_DEVICE_BAUD = "device_baudrate"
+CONF_DEVICE_PATH = "device_path"
+CONF_RELAY_ADDR = "zone_relayaddr"
+CONF_RELAY_CHAN = "zone_relaychan"
+CONF_ZONE_LOOP = "zone_loop"
+CONF_ZONE_NAME = "zone_name"
+CONF_ZONE_NUMBER = "zone_number"
+CONF_ZONE_RFID = "zone_rfid"
+CONF_ZONE_TYPE = "zone_type"
+
+DATA_AD = "alarmdecoder"
+DATA_REMOVE_STOP_LISTENER = "rm_stop_listener"
+DATA_REMOVE_UPDATE_LISTENER = "rm_update_listener"
+DATA_RESTART = "restart"
+
+DEFAULT_ALT_NIGHT_MODE = False
+DEFAULT_AUTO_BYPASS = False
+DEFAULT_CODE_ARM_REQUIRED = True
+DEFAULT_DEVICE_BAUD = 115200
+DEFAULT_DEVICE_HOST = "alarmdecoder"
+DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
+DEFAULT_DEVICE_PORT = 10000
+DEFAULT_ZONE_TYPE = "window"
+
+DEFAULT_ARM_OPTIONS = {
+ CONF_ALT_NIGHT_MODE: DEFAULT_ALT_NIGHT_MODE,
+ CONF_AUTO_BYPASS: DEFAULT_AUTO_BYPASS,
+ CONF_CODE_ARM_REQUIRED: DEFAULT_CODE_ARM_REQUIRED,
+}
+DEFAULT_ZONE_OPTIONS = {}
+
+DOMAIN = "alarmdecoder"
+
+OPTIONS_ARM = "arm_options"
+OPTIONS_ZONES = "zone_options"
+
+PROTOCOL_SERIAL = "serial"
+PROTOCOL_SOCKET = "socket"
+
+SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
+SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
+SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
+SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
+SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json
index ea2c3fb01c83f5..1697858718d273 100644
--- a/homeassistant/components/alarmdecoder/manifest.json
+++ b/homeassistant/components/alarmdecoder/manifest.json
@@ -3,5 +3,6 @@
"name": "AlarmDecoder",
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
"requirements": ["adext==0.3"],
- "codeowners": ["@ajschmidt8"]
+ "codeowners": ["@ajschmidt8"],
+ "config_flow": true
}
diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py
index 96e5feb532d313..4ce953af1d4fd6 100644
--- a/homeassistant/components/alarmdecoder/sensor.py
+++ b/homeassistant/components/alarmdecoder/sensor.py
@@ -1,26 +1,29 @@
"""Support for AlarmDecoder sensors (Shows Panel Display)."""
import logging
+from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import HomeAssistantType
-from . import SIGNAL_PANEL_MESSAGE
+from .const import SIGNAL_PANEL_MESSAGE
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up for AlarmDecoder sensor devices."""
- _LOGGER.debug("AlarmDecoderSensor: setup_platform")
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+):
+ """Set up for AlarmDecoder sensor."""
- device = AlarmDecoderSensor(hass)
-
- add_entities([device])
+ entity = AlarmDecoderSensor()
+ async_add_entities([entity])
+ return True
class AlarmDecoderSensor(Entity):
"""Representation of an AlarmDecoder keypad."""
- def __init__(self, hass):
+ def __init__(self):
"""Initialize the alarm panel."""
self._display = ""
self._state = None
diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml
index bcf5a927713aac..37c7ddf210c1c2 100644
--- a/homeassistant/components/alarmdecoder/services.yaml
+++ b/homeassistant/components/alarmdecoder/services.yaml
@@ -1,6 +1,9 @@
alarm_keypress:
description: Send custom keypresses to the alarm.
fields:
+ entity_id:
+ description: Name of alarm control panel to deliver keypress.
+ example: "alarm_control_panel.main"
keypress:
description: "String to send to the alarm panel."
example: "*71"
@@ -8,6 +11,9 @@ alarm_keypress:
alarm_toggle_chime:
description: Send the alarm the toggle chime command.
fields:
+ entity_id:
+ description: Name of alarm control panel to toggle chime.
+ example: "alarm_control_panel.main"
code:
description: A required code to toggle the alarm control panel chime with.
example: 1234
diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json
new file mode 100644
index 00000000000000..ed250b92b98840
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/strings.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Choose AlarmDecoder Protocol",
+ "data": {
+ "protocol": "Protocol"
+ }
+ },
+ "protocol": {
+ "title": "Configure connection settings",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "port": "[%key:common::config_flow::data::port%]",
+ "device_baudrate": "Device Baud Rate",
+ "device_path": "Device Path"
+ }
+ }
+ },
+ "error": {
+ "service_unavailable": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "create_entry": { "default": "Successfully connected to AlarmDecoder." },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Configure AlarmDecoder",
+ "description": "What would you like to edit?",
+ "data": {
+ "edit_select": "Edit"
+ }
+ },
+ "arm_settings": {
+ "title": "Configure AlarmDecoder",
+ "data": {
+ "auto_bypass": "Auto Bypass on Arm",
+ "code_arm_required": "Code Required for Arming",
+ "alt_night_mode": "Alternative Night Mode"
+ }
+ },
+ "zone_select": {
+ "title": "Configure AlarmDecoder",
+ "description": "Enter the zone number you'd like to to add, edit, or remove.",
+ "data": {
+ "zone_number": "Zone Number"
+ }
+ },
+ "zone_details": {
+ "title": "Configure AlarmDecoder",
+ "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
+ "data": {
+ "zone_name": "Zone Name",
+ "zone_type": "Zone Type",
+ "zone_rfid": "RF Serial",
+ "zone_loop": "RF Loop",
+ "zone_relayaddr": "Relay Address",
+ "zone_relaychan": "Relay Channel"
+ }
+ }
+ },
+ "error": {
+ "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.",
+ "int": "The field below must be an integer.",
+ "loop_rfid": "RF Loop cannot be used without RF Serial.",
+ "loop_range": "RF Loop must be an integer between 1 and 4."
+ }
+ }
+}
diff --git a/homeassistant/components/alarmdecoder/translations/ca.json b/homeassistant/components/alarmdecoder/translations/ca.json
new file mode 100644
index 00000000000000..3042a991c5f330
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/ca.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "create_entry": {
+ "default": "S'ha connectat correctament amb AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Velocitat, en baudis, del dispositiu",
+ "device_path": "Ruta del dispositiu",
+ "host": "Amfitri\u00f3",
+ "port": "Port"
+ },
+ "title": "Configuraci\u00f3 dels par\u00e0metres de connexi\u00f3"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocol"
+ },
+ "title": "Selecciona el protocol d'AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "El camp seg\u00fcent ha de ser un nombre enter.",
+ "loop_range": "El bucle RF ha de ser un nombre enter entre 1 i 4.",
+ "loop_rfid": "El bucle RF no es pot utilitzar sense RF s\u00e8rie.",
+ "relay_inclusive": "L'adre\u00e7a i el canal de rel\u00e9 s\u00f3n codependents i s'han d'incloure junts."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Mode nocturn alternatiu",
+ "auto_bypass": "Bypass autom\u00e0tic en l'activaci\u00f3",
+ "code_arm_required": "Codi necessari per a l'activaci\u00f3"
+ },
+ "title": "Configuraci\u00f3 d'AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Edita"
+ },
+ "description": "Qu\u00e8 voldries editar?",
+ "title": "Configuraci\u00f3 d'AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "Bucle RF",
+ "zone_name": "Nom de la zona",
+ "zone_relayaddr": "Adre\u00e7a del rel\u00e9",
+ "zone_relaychan": "Canal del rel\u00e9",
+ "zone_rfid": "RF s\u00e8rie",
+ "zone_type": "Tipus de zona"
+ },
+ "description": "Introdueix els detalls de la zona {zone_number}. Per suprimir la zona {zone_number}, deixa el nom de la zona en blanc.",
+ "title": "Configuraci\u00f3 d'AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "N\u00famero de zona"
+ },
+ "description": "Introdueix el n\u00famero de zona que vulguis afegir, editar o eliminar.",
+ "title": "Configuraci\u00f3 d'AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/cs.json b/homeassistant/components/alarmdecoder/translations/cs.json
new file mode 100644
index 00000000000000..b42e092bb47804
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/cs.json
@@ -0,0 +1,35 @@
+{
+ "options": {
+ "step": {
+ "arm_settings": {
+ "title": "Konfigurovat AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Upravit"
+ },
+ "description": "Co chcete upravit?",
+ "title": "Konfigurovat AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "N\u00e1zev z\u00f3ny",
+ "zone_relayaddr": "Relay adresa",
+ "zone_relaychan": "Relay kan\u00e1l",
+ "zone_rfid": "RF Serial",
+ "zone_type": "Typ z\u00f3ny"
+ },
+ "description": "Zadejte podrobnosti pro z\u00f3nu {zone_number}. Chcete-li odstranit z\u00f3nu {zone_number}, ponechejte n\u00e1zev z\u00f3ny pr\u00e1zdn\u00fd.",
+ "title": "Konfigurovat AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\u010c\u00edslo z\u00f3ny"
+ },
+ "description": "Zadejte \u010d\u00edslo z\u00f3ny, kterou chcete p\u0159idat, upravit nebo odstranit.",
+ "title": "Konfigurovat AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json
new file mode 100644
index 00000000000000..69318f87b11106
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/de.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "error": {
+ "service_unavailable": "Verbindung konnte nicht hergestellt werden"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternativer Nachtmodus"
+ }
+ },
+ "init": {
+ "data": {
+ "edit_select": "Bearbeiten"
+ },
+ "description": "Was m\u00f6chtest du bearbeiten?"
+ },
+ "zone_details": {
+ "data": {
+ "zone_name": "Zonenname",
+ "zone_relayaddr": "Relais-Adresse",
+ "zone_type": "Zonentyp"
+ }
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Zonennummer"
+ },
+ "description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/el.json b/homeassistant/components/alarmdecoder/translations/el.json
new file mode 100644
index 00000000000000..44197dbe5fb767
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/el.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03b9\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7"
+ },
+ "create_entry": {
+ "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 Baud \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2",
+ "device_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2",
+ "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2",
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "user": {
+ "data": {
+ "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf"
+ },
+ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b5\u03b4\u03af\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2.",
+ "loop_range": "\u039f \u03b2\u03c1\u03cc\u03c7\u03bf\u03c2 RF \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 1 \u03ba\u03b1\u03b9 4.",
+ "loop_rfid": "\u039f \u03b2\u03c1\u03cc\u03c7\u03bf\u03c2 RF \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c7\u03c9\u03c1\u03af\u03c2 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF.",
+ "relay_inclusive": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad \u03ba\u03b1\u03b9 \u03c4\u03bf \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03b5\u03be\u03b1\u03c1\u03c4\u03ce\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b1\u03b6\u03af."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bd\u03c5\u03c7\u03c4\u03b5\u03c1\u03b9\u03bd\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1",
+ "auto_bypass": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c4\u03bf\u03bd \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc",
+ "code_arm_required": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc"
+ },
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\u0395\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1"
+ },
+ "description": "\u03a4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5;",
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "\u0392\u03c1\u03cc\u03c7\u03bf\u03c2 RF",
+ "zone_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03b6\u03ce\u03bd\u03b7\u03c2",
+ "zone_relayaddr": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad",
+ "zone_relaychan": "\u039a\u03b1\u03bd\u03ac\u03bb\u03b9 \u03c1\u03b5\u03bb\u03ad",
+ "zone_rfid": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF",
+ "zone_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7 {zone_number}. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7 {zone_number}, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b6\u03ce\u03bd\u03b7\u03c2.",
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03b6\u03ce\u03bd\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5, \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5.",
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant/components/alarmdecoder/translations/en.json
new file mode 100644
index 00000000000000..66301414cc8c0a
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/en.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "create_entry": {
+ "default": "Successfully connected to AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Failed to connect"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Device Baud Rate",
+ "device_path": "Device Path",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Configure connection settings"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocol"
+ },
+ "title": "Choose AlarmDecoder Protocol"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "The field below must be an integer.",
+ "loop_range": "RF Loop must be an integer between 1 and 4.",
+ "loop_rfid": "RF Loop cannot be used without RF Serial.",
+ "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternative Night Mode",
+ "auto_bypass": "Auto Bypass on Arm",
+ "code_arm_required": "Code Required for Arming"
+ },
+ "title": "Configure AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Edit"
+ },
+ "description": "What would you like to edit?",
+ "title": "Configure AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "Zone Name",
+ "zone_relayaddr": "Relay Address",
+ "zone_relaychan": "Relay Channel",
+ "zone_rfid": "RF Serial",
+ "zone_type": "Zone Type"
+ },
+ "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
+ "title": "Configure AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Zone Number"
+ },
+ "description": "Enter the zone number you'd like to to add, edit, or remove.",
+ "title": "Configure AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/es.json b/homeassistant/components/alarmdecoder/translations/es.json
new file mode 100644
index 00000000000000..5b4670306daddc
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/es.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo AlarmDecoder ya est\u00e1 configurado."
+ },
+ "create_entry": {
+ "default": "Conectado con \u00e9xito a AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "No se pudo conectar"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Velocidad en baudios del dispositivo",
+ "device_path": "Ruta del dispositivo",
+ "host": "Host",
+ "port": "Puerto"
+ },
+ "title": "Configurar los ajustes de conexi\u00f3n"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocolo"
+ },
+ "title": "Elige el protocolo del AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "El campo siguiente debe ser un n\u00famero entero.",
+ "loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.",
+ "loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.",
+ "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Modo noche alternativo",
+ "auto_bypass": "Desv\u00edo autom\u00e1tico al armar",
+ "code_arm_required": "C\u00f3digo requerido para el armado"
+ },
+ "title": "Configurar AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Editar"
+ },
+ "description": "\u00bfQu\u00e9 te gustar\u00eda editar?",
+ "title": "Configurar AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "Bucle RF",
+ "zone_name": "Nombre de zona",
+ "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n",
+ "zone_relaychan": "Canal de retransmisi\u00f3n",
+ "zone_rfid": "Serie RF",
+ "zone_type": "Tipo de zona"
+ },
+ "description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.",
+ "title": "Configurar AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "N\u00famero de zona"
+ },
+ "description": "Introduce el n\u00famero de zona que deseas a\u00f1adir, editar o eliminar.",
+ "title": "Configurar AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant/components/alarmdecoder/translations/et.json
new file mode 100644
index 00000000000000..9fb7fd7fb269c8
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/et.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternatiivne \u00f6\u00f6re\u017eiim",
+ "auto_bypass": "Automaatne m\u00f6\u00f6daviik valvestamisel",
+ "code_arm_required": "Valvestamise kood"
+ },
+ "title": "Seadista AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Muuda"
+ },
+ "description": "Mida Te soovite muuta?",
+ "title": "Seadista AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF silmus",
+ "zone_name": "Ala nimi",
+ "zone_relayaddr": "Relee aadress",
+ "zone_relaychan": "Relee kanalinumber",
+ "zone_rfid": "RF jada\u00fchendus",
+ "zone_type": "Ala t\u00fc\u00fcp"
+ },
+ "description": "Sisestage ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4tke ala nimi t\u00fchjaks.",
+ "title": "Seadista AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Ala number"
+ },
+ "description": "Sisestage ala number mida soovite lisada, muuta v\u00f5i eemaldada.",
+ "title": "Seadista AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/fr.json b/homeassistant/components/alarmdecoder/translations/fr.json
new file mode 100644
index 00000000000000..c48cf00cdede4d
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/fr.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "create_entry": {
+ "default": "Connexion r\u00e9ussie \u00e0 AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "\u00c9chec de connexion"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "D\u00e9bit en bauds de l'appareil",
+ "device_path": "Chemin du p\u00e9riph\u00e9rique",
+ "host": "H\u00f4te",
+ "port": "Port"
+ },
+ "title": "Configurer les param\u00e8tres de connexion"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocole"
+ },
+ "title": "Choisissez le protocole AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Le champ ci-dessous doit \u00eatre un entier.",
+ "loop_range": "La boucle RF doit \u00eatre un entier compris entre 1 et 4.",
+ "loop_rfid": "La boucle RF ne peut pas \u00eatre utilis\u00e9e sans s\u00e9rie RF.",
+ "relay_inclusive": "L'adresse de relais et le canal de relais d\u00e9pendent du codage et doivent \u00eatre inclus ensemble."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Mode nuit alternatif",
+ "auto_bypass": "Bypass automatique \u00e0 l'armement",
+ "code_arm_required": "Code requis pour l'armement"
+ },
+ "title": "Configurer AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Modifier"
+ },
+ "description": "Que voulez-vous modifier?",
+ "title": "Configurer AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "Boucle RF",
+ "zone_name": "Nom de zone",
+ "zone_relayaddr": "Adresse de relais",
+ "zone_relaychan": "Canal de relais",
+ "zone_rfid": "RF S\u00e9rie",
+ "zone_type": "Type de zone"
+ },
+ "description": "Entrez les d\u00e9tails de la zone {zone_number} . Pour supprimer la zone {zone_number} , laissez le nom de zone vide.",
+ "title": "Configurer AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Num\u00e9ro de zone"
+ },
+ "description": "Saisissez le num\u00e9ro de zone que vous souhaitez ajouter, modifier ou supprimer.",
+ "title": "Configurer AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/it.json b/homeassistant/components/alarmdecoder/translations/it.json
new file mode 100644
index 00000000000000..ca5bf39cefde9b
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/it.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "create_entry": {
+ "default": "Collegato con successo ad AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Impossibile connettersi"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Velocit\u00e0 di trasmissione del dispositivo",
+ "device_path": "Percorso del dispositivo",
+ "host": "Host",
+ "port": "Porta"
+ },
+ "title": "Configurare le impostazioni di connessione"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocollo"
+ },
+ "title": "Scegliere il protocollo AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Il campo sottostante deve essere un numero intero.",
+ "loop_range": "Il Ciclo RF deve essere un numero intero compreso tra 1 e 4.",
+ "loop_rfid": "Il Ciclo RF non pu\u00f2 essere utilizzato senza il Seriale RF ",
+ "relay_inclusive": "L'indirizzo del rel\u00e8 e il canale del rel\u00e8 sono codipendenti e devono essere inclusi insieme."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Modalit\u00e0 notturna alternativa",
+ "auto_bypass": "Bypass automatico all'attivazione",
+ "code_arm_required": "Codice richiesto per l'attivazione"
+ },
+ "title": "Configurare AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Modifica"
+ },
+ "description": "Cosa vorresti modificare?",
+ "title": "Configurare AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "Ciclo RF",
+ "zone_name": "Nome zona",
+ "zone_relayaddr": "Indirizzo rel\u00e8",
+ "zone_relaychan": "Canale rel\u00e8",
+ "zone_rfid": "Seriale RF",
+ "zone_type": "Tipo di zona"
+ },
+ "description": "Immettere i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lasciare vuoto il campo Nome zona.",
+ "title": "Configurare AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Numero di zona"
+ },
+ "description": "Immettere il numero di zona che si desidera aggiungere, modificare o rimuovere.",
+ "title": "Configurare AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant/components/alarmdecoder/translations/ko.json
new file mode 100644
index 00000000000000..c4038572ece733
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/ko.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "service_unavailable": "\uc5f0\uacb0 \uc2e4\ud328"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "\uc7a5\uce58 \uc804\uc1a1 \uc18d\ub3c4",
+ "device_path": "\uc7a5\uce58 \uacbd\ub85c",
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ },
+ "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131"
+ },
+ "user": {
+ "data": {
+ "protocol": "\ud504\ub85c\ud1a0\ucf5c"
+ },
+ "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
+ "loop_range": "RF \ub8e8\ud504\ub294 1\uc5d0\uc11c 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
+ "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc\uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc11c\ub85c \uc758\uc874\uc801\uc774\uba70 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc",
+ "auto_bypass": "\uacbd\ube44\uc911 \uc790\ub3d9 \uc6b0\ud68c",
+ "code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc"
+ },
+ "title": "AlarmDecoder \uad6c\uc131"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\ud3b8\uc9d1"
+ },
+ "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "AlarmDecoder \uad6c\uc131"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF \ub8e8\ud504",
+ "zone_name": "\uc601\uc5ed \uc774\ub984",
+ "zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c",
+ "zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110",
+ "zone_rfid": "RF \uc2dc\ub9ac\uc5bc",
+ "zone_type": "\uc601\uc5ed \uc720\ud615"
+ },
+ "description": "{zone_number} \uc601\uc5ed\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4. {zone_number} \uc601\uc5ed\uc744 \uc0ad\uc81c\ud558\ub824\uba74 \uc601\uc5ed \uc774\ub984\uc744 \ube44\uc6cc \ub461\ub2c8\ub2e4.",
+ "title": "AlarmDecoder \uad6c\uc131"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\uad6c\uc5ed \ubc88\ud638"
+ },
+ "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uc601\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.",
+ "title": "AlarmDecoder \uad6c\uc131"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/lb.json b/homeassistant/components/alarmdecoder/translations/lb.json
new file mode 100644
index 00000000000000..082e279c764d6e
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/lb.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich mat Alarmdecoder verbonnen."
+ },
+ "error": {
+ "service_unavailable": "Feeler beim verbannen"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Apparat Baudrate",
+ "device_path": "Pad vum Apparat",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Verbindung's Optioune konfigur\u00e9ieren"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ },
+ "title": "Alarmdecoder Protokoll auswielen"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "D'Feld hei \u00ebnnen muss eng ganz Zuel sinn.",
+ "relay_inclusive": "Relais Adress a Relais Kanal sin vuneneen ofh\u00e4ngeg a musse mat abegraff sinn."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternative Nuecht Modus",
+ "auto_bypass": "Auto Bypass beim aktiv\u00e9ieren",
+ "code_arm_required": "Code erfuerderlech fir d'Aktiv\u00e9ierung"
+ },
+ "title": "AlarmDecoder konfigur\u00e9ieren"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\u00c4nneren"
+ },
+ "description": "Wat w\u00eblls du \u00e4nneren?",
+ "title": "AlarmDecoder konfigur\u00e9ieren"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Schleef",
+ "zone_name": "Numm vun der Zone",
+ "zone_relayaddr": "Relais Adresse",
+ "zone_relaychan": "Relais Kanal",
+ "zone_rfid": "RF Serielle",
+ "zone_type": "Type vun der Zone"
+ },
+ "description": "G\u00ebff Detailer fir Zone {zone_number} an. Fir Zone {zone_number} ze l\u00e4schen, loss den Numm vun der Zone eidel.",
+ "title": "AlarmDecoder konfigur\u00e9ieren"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Zone Nummer"
+ },
+ "description": "G\u00ebff d'Zonennummer an d\u00e9is Du w\u00eblls b\u00e4isetzen, \u00e4nneren oder l\u00e4schen.",
+ "title": "AlarmDecoder konfigur\u00e9ieren"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json
new file mode 100644
index 00000000000000..6091d4c4bd717f
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/nl.json
@@ -0,0 +1,73 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "AlarmDecoder-apparaat is al geconfigureerd."
+ },
+ "create_entry": {
+ "default": "Succesvol verbonden met AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Kon niet verbinden"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Baudrate van apparaat",
+ "device_path": "Apparaatpad",
+ "host": "Host",
+ "port": "Poort"
+ },
+ "title": "Configureer de verbindingsinstellingen"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocol"
+ },
+ "title": "Kies AlarmDecoder Protocol"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Het onderstaande veld moet een geheel getal zijn.",
+ "loop_range": "RF Lus moet een geheel getal zijn tussen 1 en 4.",
+ "loop_rfid": "RF Lus kan niet worden gebruikt zonder RF Serieel.",
+ "relay_inclusive": "Het relais-adres en het relais-kanaal zijn codeafhankelijk en moeten samen worden opgenomen."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternatieve nachtmodus",
+ "auto_bypass": "Automatische bypass bij inschakelen",
+ "code_arm_required": "Code vereist voor inschakelen"
+ },
+ "title": "Configureer AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Bewerk"
+ },
+ "description": "Wat wilt u bewerken?",
+ "title": "Configureer AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Lus",
+ "zone_name": "Zone naam",
+ "zone_relayaddr": "Relais Adres",
+ "zone_relaychan": "Relais Kanaal",
+ "zone_rfid": "RF Serieel",
+ "zone_type": "Zone Type"
+ },
+ "title": "Configureer AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Zone nummer"
+ },
+ "description": "Voer het zone nummer in dat u wilt toevoegen, bewerken of verwijderen.",
+ "title": "Configureer AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/no.json b/homeassistant/components/alarmdecoder/translations/no.json
new file mode 100644
index 00000000000000..36c5f21c60c7df
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/no.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "create_entry": {
+ "default": "Vellykket koblet til AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Tilkobling mislyktes."
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Baud-hastighet for enhet",
+ "device_path": "Bane til enheten",
+ "host": "Vert",
+ "port": "Port"
+ },
+ "title": "Konfigurer tilkoblingsinnstillinger"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ },
+ "title": "Velg AlarmDecoder Protokoll"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Feltet nedenfor m\u00e5 v\u00e6re et helt tall.",
+ "loop_range": "RF Loop m\u00e5 v\u00e6re et heltall mellom 1 og 4.",
+ "loop_rfid": "RF Loop kan ikke brukes uten RF Serial.",
+ "relay_inclusive": "Rel\u00e9adresse og rel\u00e9kanal er kodeavhengige og m\u00e5 inkluderes sammen."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternativ nattmodus",
+ "auto_bypass": "Auto bypass p\u00e5 Arm",
+ "code_arm_required": "Kode kreves for tilkobling"
+ },
+ "title": "Konfigurer AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Rediger"
+ },
+ "description": "Hva \u00f8nsker du \u00e5 redigere?",
+ "title": "Konfigurer AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "Sonenavn",
+ "zone_relayaddr": "Rel\u00e9 adresse",
+ "zone_relaychan": "Rel\u00e9 kanal",
+ "zone_rfid": "RF seriell",
+ "zone_type": "Sone type"
+ },
+ "description": "Angi detaljer for sonen {zone_number}. Hvis du vil slette sonen {zone_number}, lar du Sonenavn st\u00e5 tomt.",
+ "title": "Konfigurer AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Sone nummer"
+ },
+ "description": "Angi sonenummeret du vil legge til, redigere eller fjerne.",
+ "title": "Konfigurer AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/pl.json b/homeassistant/components/alarmdecoder/translations/pl.json
new file mode 100644
index 00000000000000..38438b57de6f90
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/pl.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie po\u0142\u0105czono z AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Szybko\u015b\u0107 transmisji urz\u0105dzenia (Baud Rate)",
+ "device_path": "\u015acie\u017cka urz\u0105dzenia",
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port"
+ },
+ "title": "Konfiguracja ustawie\u0144 po\u0142\u0105czenia"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protok\u00f3\u0142"
+ },
+ "title": "Wybierz protok\u00f3\u0142 AlarmDecodera"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Poni\u017csze pole musi by\u0107 liczb\u0105 ca\u0142kowit\u0105.",
+ "loop_range": "P\u0119tla RF (RF Loop) musi by\u0107 liczb\u0105 ca\u0142kowit\u0105 od 1 do 4.",
+ "relay_inclusive": "Adres przeka\u017anika i kana\u0142 przeka\u017anika s\u0105 wsp\u00f3\u0142zale\u017cne i musz\u0105 by\u0107 zawarte razem."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternatywny tryb nocny",
+ "auto_bypass": "Automatyczne obej\u015bcie przy uzbrajaniu",
+ "code_arm_required": "Wymagaj kodu do uzbrojenia"
+ },
+ "title": "Konfiguracja AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Edytuj"
+ },
+ "description": "Co chcia\u0142by\u015b edytowa\u0107?",
+ "title": "Konfiguracja AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "P\u0119tla RF (RF Loop)",
+ "zone_name": "Nazwa strefy",
+ "zone_relayaddr": "Adres przeka\u017anika",
+ "zone_relaychan": "Kana\u0142 przeka\u017anika",
+ "zone_type": "Rodzaj strefy"
+ },
+ "description": "Wprowad\u017a szczeg\u00f3\u0142y dla strefy {zone_number} . Aby usun\u0105\u0107 stref\u0119 {zone_number}, pozostaw nazw\u0119 strefy pust\u0105.",
+ "title": "Konfiguracja AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Numer strefy"
+ },
+ "description": "Wprowad\u017a numer strefy, kt\u00f3r\u0105 chcesz doda\u0107, edytowa\u0107 lub usun\u0105\u0107.",
+ "title": "Konfiguracja AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/ru.json b/homeassistant/components/alarmdecoder/translations/ru.json
new file mode 100644
index 00000000000000..3a6e56686fd6bc
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/ru.json
@@ -0,0 +1,74 @@
+{
+ "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."
+ },
+ "create_entry": {
+ "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a AlarmDecoder."
+ },
+ "error": {
+ "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
+ "device_path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "user": {
+ "data": {
+ "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b"
+ },
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.",
+ "loop_range": "RF Loop \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 1 \u0434\u043e 4.",
+ "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u0435\u0437 RF Serial.",
+ "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435 \u0438 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b \u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043d\u043e\u0447\u043d\u043e\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443",
+ "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c"
+ },
+ "description": "\u0427\u0442\u043e \u0431\u044b \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c?",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b",
+ "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435",
+ "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435",
+ "zone_rfid": "RF Serial",
+ "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u044b"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0437\u043e\u043d\u044b {zone_number}. \u0427\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0437\u043e\u043d\u0443 {zone_number}, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b\" \u043f\u0443\u0441\u0442\u044b\u043c.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u044c.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json
new file mode 100644
index 00000000000000..6c9f0dbcb43f2b
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/sv.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "step": {
+ "protocol": {
+ "data": {
+ "device_path": "Enhetsv\u00e4g"
+ },
+ "title": "Konfigurera anslutningsinst\u00e4llningar"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "edit_select": "Redigera"
+ },
+ "description": "Vad vill du redigera?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json
new file mode 100644
index 00000000000000..4caf58203c8028
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "create_entry": {
+ "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002"
+ },
+ "error": {
+ "service_unavailable": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "\u8a2d\u5099\u901a\u8a0a\u7387",
+ "device_path": "\u8a2d\u5099\u8def\u5f91",
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "title": "\u8a2d\u5b9a\u9023\u7dda\u8a2d\u5b9a"
+ },
+ "user": {
+ "data": {
+ "protocol": "\u901a\u8a0a\u5354\u5b9a"
+ },
+ "title": "\u9078\u64c7 AlarmDecoder \u901a\u8a0a\u5354\u5b9a"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "\u4e0b\u65b9\u6b04\u4f4d\u5fc5\u9808\u70ba\u6574\u6578\u3002",
+ "loop_range": "RF \u8ff4\u8def\u5fc5\u9808\u70ba\u4ecb\u65bc 1 \u81f3 4 \u9593\u7684\u6574\u6578\u3002",
+ "loop_rfid": "\u5982\u679c\u6c92\u6709 RF \u5e8f\u5217\u5247\u7121\u6cd5\u4f7f\u7528 RF \u8ff4\u8def\u3002",
+ "relay_inclusive": "\u4e2d\u7e7c\u5730\u5740\u8207\u4e2d\u7e7c\u983b\u9053\u70ba\u76f8\u4e92\u4f9d\u8cf4\uff0c\u4e26\u5fc5\u9808\u4e00\u8d77\u5305\u542b\u3002"
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "\u66ff\u4ee3\u591c\u9593\u6a21\u5f0f",
+ "auto_bypass": "\u81ea\u52d5\u5ffd\u7565\u8b66\u6212",
+ "code_arm_required": "\u8b66\u6212\u9700\u8981\u4ee3\u78bc"
+ },
+ "title": "\u8a2d\u5b9a AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\u7de8\u8f2f"
+ },
+ "description": "\u662f\u5426\u8981\u9032\u884c\u7de8\u8f2f\uff1f",
+ "title": "\u8a2d\u5b9a AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF \u8ff4\u8def",
+ "zone_name": "\u5340\u57df\u540d\u7a31",
+ "zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740",
+ "zone_relaychan": "\u4e2d\u7e7c\u983b\u9053",
+ "zone_rfid": "RF \u5e8f\u5217",
+ "zone_type": "\u5340\u57df\u985e\u578b"
+ },
+ "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"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\u5340\u57df\u78bc"
+ },
+ "description": "\u8f38\u5165\u6240\u8981\u65b0\u589e\u3001\u7de8\u8f2f\u6216\u79fb\u9664\u7684\u5340\u57df\u78bc\u3002",
+ "title": "\u8a2d\u5b9a AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 6eeb3235a64714..783c7a36949a99 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -17,6 +17,7 @@
from homeassistant.components.climate import const as climate
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_ENTITY_PICTURE,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_ALARM_ARM_AWAY,
@@ -1532,7 +1533,7 @@ async def async_api_initialize_camera_stream(hass, config, directive, context):
"""Process a InitializeCameraStreams request."""
entity = directive.entity
stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls")
- camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"]
+ camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE]
try:
external_url = network.get_url(
diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py
index a61dfc02d1093e..1d06422056d932 100644
--- a/homeassistant/components/alexa/state_report.py
+++ b/homeassistant/components/alexa/state_report.py
@@ -6,7 +6,7 @@
import aiohttp
import async_timeout
-from homeassistant.const import MATCH_ALL, STATE_ON
+from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON
import homeassistant.util.dt as dt_util
from .const import API_CHANGE, Cause
@@ -109,7 +109,7 @@ async def async_send_changereport_message(
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
- if response.status == 202:
+ if response.status == HTTP_ACCEPTED:
return
response_json = json.loads(response_text)
@@ -240,7 +240,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
- if response.status == 202:
+ if response.status == HTTP_ACCEPTED:
return
response_json = json.loads(response_text)
diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py
index 0421612b7020ab..8a4c5417700160 100644
--- a/homeassistant/components/almond/config_flow.py
+++ b/homeassistant/components/almond/config_flow.py
@@ -51,7 +51,7 @@ async def async_step_user(self, user_input=None):
"""Handle a flow start."""
# Only allow 1 instance.
if self._async_current_entries():
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
return await super().async_step_user(user_input)
@@ -79,7 +79,7 @@ async def async_step_import(self, user_input: dict = None) -> dict:
"""Import data."""
# Only allow 1 instance.
if self._async_current_entries():
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
if not await async_verify_local_connection(self.hass, user_input["host"]):
self.logger.warning(
@@ -97,7 +97,7 @@ async def async_step_import(self, user_input: dict = None) -> dict:
async def async_step_hassio(self, discovery_info):
"""Receive a Hass.io discovery."""
if self._async_current_entries():
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
self.hassio_discovery = discovery_info
diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json
index e8244798e8174e..b9074ebe4e3888 100644
--- a/homeassistant/components/almond/strings.json
+++ b/homeassistant/components/almond/strings.json
@@ -1,15 +1,17 @@
{
"config": {
"step": {
- "pick_implementation": { "title": "Pick Authentication Method" },
+ "pick_implementation": {
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+ },
"hassio_confirm": {
"title": "Almond via Hass.io add-on",
"description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?"
}
},
"abort": {
- "already_setup": "You can only configure one Almond account.",
- "cannot_connect": "Unable to connect to the Almond server.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"missing_configuration": "Please check the documentation on how to set up Almond.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json
index de9fb58eabdd0c..94b1f4ea6baf2d 100644
--- a/homeassistant/components/almond/translations/es.json
+++ b/homeassistant/components/almond/translations/es.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "S\u00f3lo puede configurar una cuenta de Almond.",
"cannot_connect": "No se puede conectar al servidor Almond.",
- "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond."
+ "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json
index f39a1660bb9003..7b7f4bff1e4be6 100644
--- a/homeassistant/components/almond/translations/fr.json
+++ b/homeassistant/components/almond/translations/fr.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Vous ne pouvez configurer qu'un seul compte Almond",
"cannot_connect": "Impossible de se connecter au serveur Almond",
- "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond."
+ "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.",
+ "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json
index aa3711446307ca..6bf280230a44f0 100644
--- a/homeassistant/components/almond/translations/it.json
+++ b/homeassistant/components/almond/translations/it.json
@@ -12,7 +12,7 @@
"title": "Almond tramite il componente aggiuntivo di Hass.io"
},
"pick_implementation": {
- "title": "Seleziona metodo di autenticazione"
+ "title": "Scegli il metodo di autenticazione"
}
}
}
diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json
index 645eaafab08e26..08cb120bf9de60 100644
--- a/homeassistant/components/almond/translations/ko.json
+++ b/homeassistant/components/almond/translations/ko.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json
index 3b866a326bea77..bfcdad87e52f7c 100644
--- a/homeassistant/components/almond/translations/lb.json
+++ b/homeassistant/components/almond/translations/lb.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.",
"cannot_connect": "Kann sech net mam Almond Server verbannen.",
- "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond."
+ "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json
index 7a2a60b1a695e3..da4671f85916c8 100644
--- a/homeassistant/components/almond/translations/nl.json
+++ b/homeassistant/components/almond/translations/nl.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.",
"cannot_connect": "Kan geen verbinding maken met de Almond-server.",
- "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
+ "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json
index 3a6a89a8340236..c9da3b2303c41c 100644
--- a/homeassistant/components/almond/translations/no.json
+++ b/homeassistant/components/almond/translations/no.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Du kan bare konfigurere en Almond konto.",
"cannot_connect": "Kan ikke koble til Almond-serveren.",
- "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond."
+ "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json
index 96e3d92e060724..d5ea73a873d313 100644
--- a/homeassistant/components/almond/translations/zh-Hant.json
+++ b/homeassistant/components/almond/translations/zh-Hant.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002",
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002",
- "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002"
+ "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py
index 2b88e7ab91eb16..3d1111136521f6 100644
--- a/homeassistant/components/ambiclimate/config_flow.py
+++ b/homeassistant/components/ambiclimate/config_flow.py
@@ -53,20 +53,20 @@ def __init__(self):
async def async_step_user(self, user_input=None):
"""Handle external yaml configuration."""
if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="already_configured_account")
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
if not config:
_LOGGER.debug("No config")
- return self.async_abort(reason="no_config")
+ return self.async_abort(reason="oauth2_missing_configuration")
return await self.async_step_auth()
async def async_step_auth(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="already_configured_account")
errors = {}
@@ -88,7 +88,7 @@ async def async_step_auth(self, user_input=None):
async def async_step_code(self, code=None):
"""Received code for authentication."""
if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="already_configured_account")
token_info = await self._get_token_info(code)
diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json
index 26af78f891560e..9e01281ad307ee 100644
--- a/homeassistant/components/ambiclimate/strings.json
+++ b/homeassistant/components/ambiclimate/strings.json
@@ -14,8 +14,8 @@
"follow_link": "Please follow the link and authenticate before pressing Submit"
},
"abort": {
- "already_setup": "The Ambiclimate account is configured.",
- "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
+ "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]",
+ "oauth2_missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"access_token": "Unknown error generating an access token."
}
}
diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json
index 64ea45410d35a1..04722aebb05efa 100644
--- a/homeassistant/components/ambiclimate/translations/ca.json
+++ b/homeassistant/components/ambiclimate/translations/ca.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "S'ha produ\u00eft un error desconegut al generat un token d'acc\u00e9s.",
+ "already_configured_account": "El compte ja ha estat configurat",
"already_setup": "El compte d'Ambi Climate est\u00e0 configurat.",
- "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3."
},
"create_entry": {
"default": "Autenticaci\u00f3 exitosa amb Ambi Climate."
diff --git a/homeassistant/components/ambiclimate/translations/en.json b/homeassistant/components/ambiclimate/translations/en.json
index 177ecd2907f9de..efcc96d85dd37c 100644
--- a/homeassistant/components/ambiclimate/translations/en.json
+++ b/homeassistant/components/ambiclimate/translations/en.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "Unknown error generating an access token.",
+ "already_configured_account": "Account is already configured",
"already_setup": "The Ambiclimate account is configured.",
- "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "The component is not configured. Please follow the documentation."
},
"create_entry": {
"default": "Successfully authenticated with Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/es.json b/homeassistant/components/ambiclimate/translations/es.json
index 01d6643b6349ff..ba5a53de2ae8c0 100644
--- a/homeassistant/components/ambiclimate/translations/es.json
+++ b/homeassistant/components/ambiclimate/translations/es.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "Error desconocido al generar un token de acceso.",
+ "already_configured_account": "La cuenta ya ha sido configurada",
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
- "no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n."
},
"create_entry": {
"default": "Autenticado correctamente con Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/et.json b/homeassistant/components/ambiclimate/translations/et.json
new file mode 100644
index 00000000000000..4bca8b8fa4ed8c
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "Kasutaja on juba lisatud",
+ "oauth2_missing_configuration": "Osis pole h\u00e4\u00e4lestatud. Lisainfot saad dokumentatsioonist."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json
index 9c5864fcb0f6bd..879a02d38d00c7 100644
--- a/homeassistant/components/ambiclimate/translations/fr.json
+++ b/homeassistant/components/ambiclimate/translations/fr.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
+ "already_configured_account": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
"already_setup": "Le compte Ambiclimate est configur\u00e9.",
- "no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
},
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/it.json b/homeassistant/components/ambiclimate/translations/it.json
index 427aa0ab445f75..b53ebaab64870d 100644
--- a/homeassistant/components/ambiclimate/translations/it.json
+++ b/homeassistant/components/ambiclimate/translations/it.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "Errore sconosciuto durante la generazione di un token di accesso.",
+ "already_configured_account": "L'account \u00e8 gi\u00e0 configurato",
"already_setup": "L'account Ambiclimate \u00e8 configurato.",
- "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni](https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione."
},
"create_entry": {
"default": "Autenticato con successo con Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json
index 3cc3f4617e797f..493ad755df1576 100644
--- a/homeassistant/components/ambiclimate/translations/no.json
+++ b/homeassistant/components/ambiclimate/translations/no.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "Ukjent feil ved oppretting av tilgangstoken.",
+ "already_configured_account": "Kontoen er allerede konfigurert",
"already_setup": "Ambiclimate-kontoen er konfigurert.",
- "no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan godkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan godkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/).",
+ "oauth2_missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
},
"create_entry": {
"default": "Vellykket godkjenning med Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json
index 7a440f28bed54f..e172063d383b61 100644
--- a/homeassistant/components/ambiclimate/translations/ru.json
+++ b/homeassistant/components/ambiclimate/translations/ru.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "already_configured_account": "\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_setup": "\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.",
- "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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/components/ambiclimate/)."
+ "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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/components/ambiclimate/).",
+ "oauth2_missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/ambiclimate/translations/zh-Hant.json b/homeassistant/components/ambiclimate/translations/zh-Hant.json
index 7b995d099442b6..9227f72a306481 100644
--- a/homeassistant/components/ambiclimate/translations/zh-Hant.json
+++ b/homeassistant/components/ambiclimate/translations/zh-Hant.json
@@ -2,8 +2,10 @@
"config": {
"abort": {
"access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002",
+ "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"already_setup": "Ambiclimate \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002"
+ "no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002",
+ "oauth2_missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u8a2d\u5099\u3002"
diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
index 9428449dc75a20..68bfb85cf62c7c 100644
--- a/homeassistant/components/ambient_station/__init__.py
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -6,8 +6,10 @@
from aioambient.errors import WebsocketError
import voluptuous as vol
+from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
+ AREA_SQUARE_METERS,
ATTR_LOCATION,
ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -15,8 +17,10 @@
CONF_API_KEY,
DEGREE,
EVENT_HOMEASSISTANT_STOP,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_INHG,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
@@ -141,8 +145,8 @@
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = {
TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None),
- TYPE_BAROMABSIN: ("Abs Pressure", "inHg", TYPE_SENSOR, "pressure"),
- TYPE_BAROMRELIN: ("Rel Pressure", "inHg", TYPE_SENSOR, "pressure"),
+ TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
+ TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"),
@@ -175,16 +179,16 @@
TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
- TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"),
- TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"),
+ TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"),
TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"),
TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"),
@@ -205,8 +209,13 @@
TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOLARRADIATION: ("Solar Rad", f"{POWER_WATT}/m^2", TYPE_SENSOR, None),
- TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"),
+ TYPE_SOLARRADIATION: (
+ "Solar Rad",
+ f"{POWER_WATT}/{AREA_SQUARE_METERS}",
+ TYPE_SENSOR,
+ None,
+ ),
+ TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", LIGHT_LUX, TYPE_SENSOR, "illuminance"),
TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json
index 54b3dd055115ac..a9bce82e10b19e 100644
--- a/homeassistant/components/ambient_station/strings.json
+++ b/homeassistant/components/ambient_station/strings.json
@@ -10,11 +10,11 @@
}
},
"error": {
- "invalid_key": "Invalid API Key and/or Application Key",
+ "invalid_key": "[%key:common::config_flow::error::invalid_api_key%]",
"no_devices": "No devices found in account"
},
"abort": {
- "already_configured": "This app key is already in use."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py
index 14384565718480..377ecfec667592 100644
--- a/homeassistant/components/android_ip_webcam/binary_sensor.py
+++ b/homeassistant/components/android_ip_webcam/binary_sensor.py
@@ -1,5 +1,8 @@
"""Support for Android IP Webcam binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ BinarySensorEntity,
+)
from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity
@@ -47,4 +50,4 @@ async def async_update(self):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return "motion"
+ return DEVICE_CLASS_MOTION
diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index 8e62813714eaec..f6c773941f166b 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.2.1",
- "androidtv[async]==0.0.49",
+ "androidtv[async]==0.0.50",
"pure-python-adb[async]==0.3.0.dev0"
],
"codeowners": ["@JeffLIrion"]
diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py
index 959c85abd77799..b13a2000e5ba51 100644
--- a/homeassistant/components/androidtv/media_player.py
+++ b/homeassistant/components/androidtv/media_player.py
@@ -380,7 +380,7 @@ async def _adb_exception_catcher(self, *args, **kwargs):
# An unforeseen exception occurred. Close the ADB connection so that
# it doesn't happen over and over again, then raise the exception.
await self.aftv.adb_close()
- self._available = False # pylint: disable=protected-access
+ self._available = False
raise
return _adb_exception_catcher
@@ -476,11 +476,6 @@ def name(self):
"""Return the device name."""
return self._name
- @property
- def should_poll(self):
- """Device should be polled."""
- return True
-
@property
def source(self):
"""Return the current app."""
@@ -502,14 +497,23 @@ def unique_id(self):
return self._unique_id
@adb_decorator()
+ async def _adb_screencap(self):
+ """Take a screen capture from the device."""
+ return await self.aftv.adb_screencap()
+
async def async_get_media_image(self):
"""Fetch current playing image."""
if not self._screencap or self.state in [STATE_OFF, None] or not self.available:
return None, None
- media_data = await self.aftv.adb_screencap()
+ media_data = await self._adb_screencap()
if media_data:
return media_data, "image/png"
+
+ # If an exception occurred and the device is no longer available, write the state
+ if not self.available:
+ self.async_write_ha_state()
+
return None, None
@adb_decorator()
diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py
index c769f51d5b6b6f..0669a3bb6c6a4f 100644
--- a/homeassistant/components/anel_pwrctrl/switch.py
+++ b/homeassistant/components/anel_pwrctrl/switch.py
@@ -66,11 +66,6 @@ def __init__(self, port, parent_device):
self._port = port
self._parent_device = parent_device
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def unique_id(self):
"""Return the unique ID of the device."""
diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py
index c1f692a76ad370..34d899e996cd1f 100644
--- a/homeassistant/components/api/__init__.py
+++ b/homeassistant/components/api/__init__.py
@@ -38,6 +38,7 @@
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
+from homeassistant.helpers.system_info import async_get_system_info
_LOGGER = logging.getLogger(__name__)
@@ -45,6 +46,7 @@
ATTR_EXTERNAL_URL = "external_url"
ATTR_INTERNAL_URL = "internal_url"
ATTR_LOCATION_NAME = "location_name"
+ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"
@@ -181,6 +183,7 @@ async def get(self, request):
"""Get discovery information."""
hass = request.app["hass"]
uuid = await hass.helpers.instance_id.async_get()
+ system_info = await async_get_system_info(hass)
data = {
ATTR_UUID: uuid,
@@ -188,6 +191,7 @@ async def get(self, request):
ATTR_EXTERNAL_URL: None,
ATTR_INTERNAL_URL: None,
ATTR_LOCATION_NAME: hass.config.location_name,
+ ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,
diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json
index beb3a80ceebc81..34061120322db4 100644
--- a/homeassistant/components/apprise/manifest.json
+++ b/homeassistant/components/apprise/manifest.json
@@ -2,6 +2,6 @@
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise",
- "requirements": ["apprise==0.8.8"],
+ "requirements": ["apprise==0.8.9"],
"codeowners": ["@caronc"]
}
diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py
index 0c8c5b26eeca4b..95bf11ddc097ae 100644
--- a/homeassistant/components/apprise/notify.py
+++ b/homeassistant/components/apprise/notify.py
@@ -28,8 +28,7 @@
def get_service(hass, config, discovery_info=None):
"""Get the Apprise notification service."""
-
- # Create our object
+ # Create our Apprise Instance (reference our asset)
a_obj = apprise.Apprise()
if config.get(CONF_FILE):
diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py
index 2a9f1946cb4d98..c04757e75d3cac 100644
--- a/homeassistant/components/arcam_fmj/config_flow.py
+++ b/homeassistant/components/arcam_fmj/config_flow.py
@@ -37,7 +37,7 @@ async def _async_check_and_create(self, host, port):
try:
await client.start()
except ConnectionFailed:
- return self.async_abort(reason="unable_to_connect")
+ return self.async_abort(reason="cannot_connect")
finally:
await client.stop()
diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json
index 67aaf7a11cbdc8..b6a7d6ee559253 100644
--- a/homeassistant/components/arcam_fmj/strings.json
+++ b/homeassistant/components/arcam_fmj/strings.json
@@ -1,28 +1,28 @@
{
- "config": {
- "abort": {
- "already_configured": "Device was already setup.",
- "already_in_progress": "Config flow for device is already in progress.",
- "unable_to_connect": "Unable to connect to device."
- },
- "error": {},
- "flow_title": "Arcam FMJ on {host}",
- "step": {
- "confirm": {
- "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
- },
- "user": {
- "data": {
- "host": "[%key:common::config_flow::data::host%]",
- "port": "[%key:common::config_flow::data::port%]"
- },
- "description": "Please enter the host name or IP address of device."
- }
- }
+ "config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
- "device_automation": {
- "trigger_type": {
- "turn_on": "{entity_name} was requested to turn on"
- }
+ "error": {},
+ "flow_title": "Arcam FMJ on {host}",
+ "step": {
+ "confirm": {
+ "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
+ },
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "port": "[%key:common::config_flow::data::port%]"
+ },
+ "description": "Please enter the host name or IP address of device."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "{entity_name} was requested to turn on"
}
+ }
}
diff --git a/homeassistant/components/arcam_fmj/translations/ca.json b/homeassistant/components/arcam_fmj/translations/ca.json
index 28149d5e06edd9..98cedd0771e2f1 100644
--- a/homeassistant/components/arcam_fmj/translations/ca.json
+++ b/homeassistant/components/arcam_fmj/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja s'ha configurat.",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"unable_to_connect": "No es pot connectar amb el dispositiu."
},
"flow_title": "Arcam FMJ a {host}",
diff --git a/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant/components/arcam_fmj/translations/en.json
index b22ed592f69084..4858cb993707cd 100644
--- a/homeassistant/components/arcam_fmj/translations/en.json
+++ b/homeassistant/components/arcam_fmj/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device was already setup.",
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"unable_to_connect": "Unable to connect to device."
},
"flow_title": "Arcam FMJ on {host}",
diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json
index 61f3dd6fc47be8..1328ba1cd349c0 100644
--- a/homeassistant/components/arcam_fmj/translations/it.json
+++ b/homeassistant/components/arcam_fmj/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo era gi\u00e0 configurato.",
- "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"unable_to_connect": "Impossibile connettersi al dispositivo."
},
"error": {
diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json
index 14d55224119cfa..4d5933acd72877 100644
--- a/homeassistant/components/arcam_fmj/translations/no.json
+++ b/homeassistant/components/arcam_fmj/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten var allerede konfigurert.",
- "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"unable_to_connect": "Kan ikke koble til enheten."
},
"flow_title": "Arcam FMJ p\u00e5 {host}",
diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json
index 7b2d5da76e52fe..7cc66881cdfda9 100644
--- a/homeassistant/components/arcam_fmj/translations/pl.json
+++ b/homeassistant/components/arcam_fmj/translations/pl.json
@@ -1,14 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem."
},
+ "flow_title": "Arcam FMJ na {host}",
"step": {
+ "confirm": {
+ "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistant?"
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"port": "Port"
- }
+ },
+ "description": "Wpisz nazw\u0119 hosta lub adres IP urz\u0105dzenia."
}
}
},
diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json
index 807bfb4e4c577c..f3dac175812bc2 100644
--- a/homeassistant/components/arcam_fmj/translations/ru.json
+++ b/homeassistant/components/arcam_fmj/translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"unable_to_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"flow_title": "Arcam FMJ {host}",
diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json
index f25906f0d815bb..8e9bfad987e455 100644
--- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json
+++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u8a2d\u5b9a\u3002",
- "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"unable_to_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u8a2d\u5099\u3002"
},
"flow_title": "Arcam FMJ \uff08{host}\uff09",
diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py
index e87a625522e11f..2890fd4abdaf38 100644
--- a/homeassistant/components/arduino/__init__.py
+++ b/homeassistant/components/arduino/__init__.py
@@ -23,6 +23,12 @@
def setup(hass, config):
"""Set up the Arduino component."""
+ _LOGGER.warning(
+ "The %s integration has been deprecated. Please move your "
+ "configuration to the firmata integration. "
+ "https://www.home-assistant.io/integrations/firmata",
+ DOMAIN,
+ )
port = config[DOMAIN][CONF_PORT]
diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py
index 7c947be61bf686..3db1283279e684 100644
--- a/homeassistant/components/arwn/sensor.py
+++ b/homeassistant/components/arwn/sensor.py
@@ -96,7 +96,7 @@ def async_sensor_event_received(msg):
store[sensor.name] = sensor
_LOGGER.debug(
"Registering new sensor %(name)s => %(event)s",
- dict(name=sensor.name, event=event),
+ {"name": sensor.name, "event": event},
)
async_add_entities((sensor,), True)
else:
diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json
index 9f8a5d2c6ebe5a..5e94afb06d31cb 100644
--- a/homeassistant/components/atag/manifest.json
+++ b/homeassistant/components/atag/manifest.json
@@ -3,6 +3,6 @@
"name": "Atag",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag/",
- "requirements": ["pyatag==0.3.3.4"],
+ "requirements": ["pyatag==0.3.4.4"],
"codeowners": ["@MatsNL"]
}
diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json
index 85e22c10c1be3d..22d7092142007f 100644
--- a/homeassistant/components/atag/strings.json
+++ b/homeassistant/components/atag/strings.json
@@ -6,14 +6,14 @@
"title": "Connect to the device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
- "email": "Email (Optional)",
+ "email": "[%key:common::config_flow::data::email%]",
"port": "[%key:common::config_flow::data::port%]"
}
}
},
"error": {
"unauthorized": "Pairing denied, check device for auth request",
- "connection_error": "Failed to connect, please try again"
+ "connection_error": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "This device has already been added to HomeAssistant"
diff --git a/homeassistant/components/atag/translations/ca.json b/homeassistant/components/atag/translations/ca.json
index 6677e7eaa4d4e6..d15cf197ffefd9 100644
--- a/homeassistant/components/atag/translations/ca.json
+++ b/homeassistant/components/atag/translations/ca.json
@@ -4,13 +4,13 @@
"already_configured": "Aquest dispositiu ja ha estat afegit a Home Assistant"
},
"error": {
- "connection_error": "No s'ha pogut connectar, torna-ho a provar",
+ "connection_error": "Ha fallat la connexi\u00f3",
"unauthorized": "La vinculaci\u00f3 s'ha denegat, comprova si hi ha una sol\u00b7licitud d'autenticaci\u00f3 al dispositiu"
},
"step": {
"user": {
"data": {
- "email": "Correu electr\u00f2nic (opcional)",
+ "email": "Correu electr\u00f2nic",
"host": "Amfitri\u00f3",
"port": "Port"
},
diff --git a/homeassistant/components/atag/translations/en.json b/homeassistant/components/atag/translations/en.json
index 8901a417065b6a..3bd41082395def 100644
--- a/homeassistant/components/atag/translations/en.json
+++ b/homeassistant/components/atag/translations/en.json
@@ -4,13 +4,13 @@
"already_configured": "This device has already been added to HomeAssistant"
},
"error": {
- "connection_error": "Failed to connect, please try again",
+ "connection_error": "Failed to connect",
"unauthorized": "Pairing denied, check device for auth request"
},
"step": {
"user": {
"data": {
- "email": "Email (Optional)",
+ "email": "Email",
"host": "Host",
"port": "Port"
},
diff --git a/homeassistant/components/atag/translations/it.json b/homeassistant/components/atag/translations/it.json
index cbef4fab268cd6..aee7a323ef5875 100644
--- a/homeassistant/components/atag/translations/it.json
+++ b/homeassistant/components/atag/translations/it.json
@@ -4,13 +4,13 @@
"already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato aggiunto a HomeAssistant"
},
"error": {
- "connection_error": "Impossibile connettersi, si prega di riprovare",
+ "connection_error": "Impossibile connettersi",
"unauthorized": "Associazione negata, controllare il dispositivo per la richiesta di autenticazione"
},
"step": {
"user": {
"data": {
- "email": "E-mail (Opzionale)",
+ "email": "E-mail",
"host": "Host",
"port": "Porta"
},
diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json
index a0e428f286a274..690a8ce661ae67 100644
--- a/homeassistant/components/atag/translations/no.json
+++ b/homeassistant/components/atag/translations/no.json
@@ -4,13 +4,13 @@
"already_configured": "Denne enheten er allerede lagt til i HomeAssistant"
},
"error": {
- "connection_error": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "connection_error": "Tilkobling mislyktes.",
"unauthorized": "Parring nektet, sjekk enheten for autorisasjonsforesp\u00f8rsel"
},
"step": {
"user": {
"data": {
- "email": "E-post (valgfritt)",
+ "email": "E-post",
"host": "Vert",
"port": ""
},
diff --git a/homeassistant/components/atag/translations/pl.json b/homeassistant/components/atag/translations/pl.json
index d7d633aeda5cb0..37291614bee92c 100644
--- a/homeassistant/components/atag/translations/pl.json
+++ b/homeassistant/components/atag/translations/pl.json
@@ -4,7 +4,8 @@
"already_configured": "To urz\u0105dzenie zosta\u0142o ju\u017c dodane do Home Assistant"
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie."
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
+ "unauthorized": "Odmowa parowania, sprawd\u017a urz\u0105dzenie pod k\u0105tem \u017c\u0105dania autoryzacji."
},
"step": {
"user": {
diff --git a/homeassistant/components/atag/translations/ru.json b/homeassistant/components/atag/translations/ru.json
index caf80b415e625d..c7bdcead280c17 100644
--- a/homeassistant/components/atag/translations/ru.json
+++ b/homeassistant/components/atag/translations/ru.json
@@ -4,13 +4,13 @@
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
- "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unauthorized": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"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 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442"
},
diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json
index 6fc2e520d17dbd..5e3590f7a5dfd1 100644
--- a/homeassistant/components/atag/translations/zh-Hant.json
+++ b/homeassistant/components/atag/translations/zh-Hant.json
@@ -4,13 +4,13 @@
"already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u65b0\u589e\u81f3 Home Assistant"
},
"error": {
- "connection_error": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "connection_error": "\u9023\u7dda\u5931\u6557",
"unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a8d\u8b49\u8acb\u6c42"
},
"step": {
"user": {
"data": {
- "email": "\u90f5\u4ef6\uff08\u9078\u9805\uff09",
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0"
},
diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py
index e0d7749dcbb42f..feaf61450e8b34 100644
--- a/homeassistant/components/august/__init__.py
+++ b/homeassistant/components/august/__init__.py
@@ -3,13 +3,18 @@
import itertools
import logging
-from aiohttp import ClientError
+from aiohttp import ClientError, ClientResponseError
from august.authenticator import ValidationResult
from august.exceptions import AugustApiAIOHTTPError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv
@@ -29,7 +34,7 @@
MIN_TIME_BETWEEN_DETAIL_UPDATES,
VERIFICATION_CODE_KEY,
)
-from .exceptions import InvalidAuth, RequireValidation
+from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin
@@ -113,10 +118,7 @@ async def async_setup_august(hass, config_entry, august_gateway):
await august_gateway.async_authenticate()
except RequireValidation:
await async_request_validation(hass, config_entry, august_gateway)
- return False
- except InvalidAuth:
- _LOGGER.error("Password is no longer valid. Please set up August again")
- return False
+ raise
# We still use the configurator to get a new 2fa code
# when needed since config_flow doesn't have a way
@@ -171,8 +173,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
try:
await august_gateway.async_setup(entry.data)
return await async_setup_august(hass, entry, august_gateway)
- except asyncio.TimeoutError as err:
+ except ClientResponseError as err:
+ if err.status == HTTP_UNAUTHORIZED:
+ _async_start_reauth(hass, entry)
+ return False
+
raise ConfigEntryNotReady from err
+ except InvalidAuth:
+ _async_start_reauth(hass, entry)
+ return False
+ except RequireValidation:
+ return False
+ except (CannotConnect, asyncio.TimeoutError) as err:
+ raise ConfigEntryNotReady from err
+
+
+def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth"},
+ data=entry.data,
+ )
+ )
+ _LOGGER.error("Password is no longer valid. Please reauthenticate")
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py
index bf6f1d9cd8142d..f595479c0cfd65 100644
--- a/homeassistant/components/august/config_flow.py
+++ b/homeassistant/components/august/config_flow.py
@@ -4,7 +4,7 @@
from august.authenticator import ValidationResult
import voluptuous as vol
-from homeassistant import config_entries, core
+from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from .const import (
@@ -19,18 +19,8 @@
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
- vol.Required(CONF_USERNAME): str,
- vol.Required(CONF_PASSWORD): str,
- vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
- }
-)
-
async def async_validate_input(
- hass: core.HomeAssistant,
data,
august_gateway,
):
@@ -79,6 +69,7 @@ def __init__(self):
"""Store an AugustGateway()."""
self._august_gateway = None
self.user_auth_details = {}
+ self._needs_reset = False
super().__init__()
async def async_step_user(self, user_input=None):
@@ -87,30 +78,45 @@ async def async_step_user(self, user_input=None):
self._august_gateway = AugustGateway(self.hass)
errors = {}
if user_input is not None:
- await self._august_gateway.async_setup(user_input)
+ combined_inputs = {**self.user_auth_details, **user_input}
+ await self._august_gateway.async_setup(combined_inputs)
+ if self._needs_reset:
+ self._needs_reset = False
+ await self._august_gateway.async_reset_authentication()
try:
info = await async_validate_input(
- self.hass,
- user_input,
+ combined_inputs,
self._august_gateway,
)
- await self.async_set_unique_id(user_input[CONF_USERNAME])
- return self.async_create_entry(title=info["title"], data=info["data"])
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except RequireValidation:
- self.user_auth_details = user_input
+ self.user_auth_details.update(user_input)
return await self.async_step_validation()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
+ if not errors:
+ self.user_auth_details.update(user_input)
+
+ existing_entry = await self.async_set_unique_id(
+ combined_inputs[CONF_USERNAME]
+ )
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data=info["data"]
+ )
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+ return self.async_create_entry(title=info["title"], data=info["data"])
+
return self.async_show_form(
- step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ step_id="user", data_schema=self._async_build_schema(), errors=errors
)
async def async_step_validation(self, user_input=None):
@@ -135,3 +141,23 @@ async def async_step_import(self, user_input):
self._abort_if_unique_id_configured()
return await self.async_step_user(user_input)
+
+ async def async_step_reauth(self, data):
+ """Handle configuration by re-auth."""
+ self.user_auth_details = dict(data)
+ self._needs_reset = True
+ return await self.async_step_user()
+
+ def _async_build_schema(self):
+ """Generate the config flow schema."""
+ base_schema = {
+ vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
+ }
+ for key in self.user_auth_details:
+ if key == CONF_PASSWORD or key not in base_schema:
+ continue
+ del base_schema[key]
+ return vol.Schema(base_schema)
diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py
index 6918907611ffda..b72bb52e710925 100644
--- a/homeassistant/components/august/gateway.py
+++ b/homeassistant/components/august/gateway.py
@@ -2,12 +2,18 @@
import asyncio
import logging
+import os
-from aiohttp import ClientError
+from aiohttp import ClientError, ClientResponseError
from august.api_async import ApiAsync
from august.authenticator_async import AuthenticationState, AuthenticatorAsync
-from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers import aiohttp_client
from .const import (
@@ -32,29 +38,14 @@ def __init__(self, hass):
self._access_token_cache_file = None
self._hass = hass
self._config = None
- self._api = None
- self._authenticator = None
- self._authentication = None
-
- @property
- def authenticator(self):
- """August authentication object from py-august."""
- return self._authenticator
-
- @property
- def authentication(self):
- """August authentication object from py-august."""
- return self._authentication
+ self.api = None
+ self.authenticator = None
+ self.authentication = None
@property
def access_token(self):
"""Access token for the api."""
- return self._authentication.access_token
-
- @property
- def api(self):
- """August api object from py-august."""
- return self._api
+ return self.authentication.access_token
def config_entry(self):
"""Config entry."""
@@ -78,12 +69,12 @@ async def async_setup(self, conf):
)
self._config = conf
- self._api = ApiAsync(
+ self.api = ApiAsync(
self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
)
- self._authenticator = AuthenticatorAsync(
- self._api,
+ self.authenticator = AuthenticatorAsync(
+ self.api,
self._config[CONF_LOGIN_METHOD],
self._config[CONF_USERNAME],
self._config[CONF_PASSWORD],
@@ -93,30 +84,47 @@ async def async_setup(self, conf):
),
)
- await self._authenticator.async_setup_authentication()
+ await self.authenticator.async_setup_authentication()
async def async_authenticate(self):
"""Authenticate with the details provided to setup."""
- self._authentication = None
+ self.authentication = None
try:
- self._authentication = await self.authenticator.async_authenticate()
+ self.authentication = await self.authenticator.async_authenticate()
+ if self.authentication.state == AuthenticationState.AUTHENTICATED:
+ # Call the locks api to verify we are actually
+ # authenticated because we can be authenticated
+ # by have no access
+ await self.api.async_get_operable_locks(self.access_token)
+ except ClientResponseError as ex:
+ if ex.status == HTTP_UNAUTHORIZED:
+ raise InvalidAuth from ex
+
+ raise CannotConnect from ex
except ClientError as ex:
_LOGGER.error("Unable to connect to August service: %s", str(ex))
raise CannotConnect from ex
- if self._authentication.state == AuthenticationState.BAD_PASSWORD:
+ if self.authentication.state == AuthenticationState.BAD_PASSWORD:
raise InvalidAuth
- if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION:
+ if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION:
raise RequireValidation
- if self._authentication.state != AuthenticationState.AUTHENTICATED:
- _LOGGER.error(
- "Unknown authentication state: %s", self._authentication.state
- )
+ if self.authentication.state != AuthenticationState.AUTHENTICATED:
+ _LOGGER.error("Unknown authentication state: %s", self.authentication.state)
raise InvalidAuth
- return self._authentication
+ return self.authentication
+
+ async def async_reset_authentication(self):
+ """Remove the cache file."""
+ await self._hass.async_add_executor_job(self._reset_authentication)
+
+ def _reset_authentication(self):
+ """Remove the cache file."""
+ if os.path.exists(self._access_token_cache_file):
+ os.unlink(self._access_token_cache_file)
async def async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
@@ -130,4 +138,4 @@ async def async_refresh_access_token_if_needed(self):
self.authentication.access_token_expires,
refreshed_authentication.access_token_expires,
)
- self._authentication = refreshed_authentication
+ self.authentication = refreshed_authentication
diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json
index 880c13c7fe278c..998d870e629e03 100644
--- a/homeassistant/components/august/strings.json
+++ b/homeassistant/components/august/strings.json
@@ -1,12 +1,13 @@
{
"config": {
"error": {
- "unknown": "Unexpected error",
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication"
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "Account is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"step": {
"validation": {
@@ -28,4 +29,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json
index 4f8f9cebe63ec6..9d3e3535e46c6d 100644
--- a/homeassistant/components/august/translations/ca.json
+++ b/homeassistant/components/august/translations/ca.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "El compte ja ha estat configurat"
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/august/translations/el.json b/homeassistant/components/august/translations/el.json
new file mode 100644
index 00000000000000..08f214f7386300
--- /dev/null
+++ b/homeassistant/components/august/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json
index b8bf1b1bc03c7e..c6c19321d8a5a0 100644
--- a/homeassistant/components/august/translations/en.json
+++ b/homeassistant/components/august/translations/en.json
@@ -1,10 +1,11 @@
{
"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, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json
index 28d9743c073a8c..2ec72c9e4eb8bf 100644
--- a/homeassistant/components/august/translations/es.json
+++ b/homeassistant/components/august/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "La cuenta ya est\u00e1 configurada"
+ "already_configured": "La cuenta ya est\u00e1 configurada",
+ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
},
"error": {
"cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json
new file mode 100644
index 00000000000000..af21b9d8204a6d
--- /dev/null
+++ b/homeassistant/components/august/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Taasautentimine \u00f5nnestus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json
index 752b7dc3712656..82568b681fdf48 100644
--- a/homeassistant/components/august/translations/fr.json
+++ b/homeassistant/components/august/translations/fr.json
@@ -1,7 +1,8 @@
{
"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": "Impossible de se connecter, veuillez r\u00e9essayer",
diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json
index 3a5f2676acdb39..08332c29d7ecdf 100644
--- a/homeassistant/components/august/translations/it.json
+++ b/homeassistant/components/august/translations/it.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La riautenticazione ha avuto successo"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json
index 52f939c45a0a76..c11bc55ec40dde 100644
--- a/homeassistant/components/august/translations/ko.json
+++ b/homeassistant/components/august/translations/ko.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc774 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4."
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
diff --git a/homeassistant/components/august/translations/lb.json b/homeassistant/components/august/translations/lb.json
index 501af05c2dff7d..dbc71325a816c5 100644
--- a/homeassistant/components/august/translations/lb.json
+++ b/homeassistant/components/august/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Kont ass scho konfigur\u00e9iert"
+ "already_configured": "Kont ass scho konfigur\u00e9iert",
+ "reauth_successful": "Re-Authentifikatioun erfollegr\u00e4ich"
},
"error": {
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json
index 1697f634d9a82e..e48d27801ccdc7 100644
--- a/homeassistant/components/august/translations/nl.json
+++ b/homeassistant/components/august/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Account al geconfigureerd"
+ "already_configured": "Account al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
"cannot_connect": "Verbinding mislukt, probeer het opnieuw",
diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json
index 838508f132dc30..764aa5624a63bf 100644
--- a/homeassistant/components/august/translations/no.json
+++ b/homeassistant/components/august/translations/no.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Reautentisering var vellykket"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json
index eeaa5269da45c3..1e046c286f1d6e 100644
--- a/homeassistant/components/august/translations/pl.json
+++ b/homeassistant/components/august/translations/pl.json
@@ -1,12 +1,13 @@
{
"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, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json
index 9a49caed547bee..9ea0b531bf8b0c 100644
--- a/homeassistant/components/august/translations/ru.json
+++ b/homeassistant/components/august/translations/ru.json
@@ -1,10 +1,11 @@
{
"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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json
index 6b7e206d4c4f8c..667d881465962f 100644
--- a/homeassistant/components/august/translations/zh-Hant.json
+++ b/homeassistant/components/august/translations/zh-Hant.json
@@ -1,10 +1,11 @@
{
"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\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 31e3b7ea648d9d..725450a0a1208d 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -80,7 +80,11 @@
)
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
-from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND
+from homeassistant.const import (
+ HTTP_BAD_REQUEST,
+ HTTP_METHOD_NOT_ALLOWED,
+ HTTP_NOT_FOUND,
+)
from . import indieauth
@@ -153,7 +157,7 @@ def __init__(self, flow_mgr, store_result):
async def get(self, request):
"""Do not allow index of flows in progress."""
- return web.Response(status=405)
+ return web.Response(status=HTTP_METHOD_NOT_ALLOWED)
@RequestDataValidator(
vol.Schema(
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index 2ac2b8d9354128..3a296178aeb9c0 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -10,7 +10,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.condition import async_validate_condition_config
-from homeassistant.helpers.script import async_validate_action_config
+from homeassistant.helpers.script import async_validate_actions_config
from homeassistant.helpers.trigger import async_validate_trigger_config
from homeassistant.loader import IntegrationNotFound
@@ -36,9 +36,7 @@ async def async_validate_config_item(hass, config, full_config=None):
]
)
- config[CONF_ACTION] = await asyncio.gather(
- *[async_validate_action_config(hass, action) for action in config[CONF_ACTION]]
- )
+ config[CONF_ACTION] = await async_validate_actions_config(hass, config[CONF_ACTION])
return config
diff --git a/homeassistant/components/avri/translations/pl.json b/homeassistant/components/avri/translations/pl.json
index 0881b1ec26a4af..088edbe79371ec 100644
--- a/homeassistant/components/avri/translations/pl.json
+++ b/homeassistant/components/avri/translations/pl.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ten adres jest ju\u017c skonfigurowany."
+ },
"error": {
+ "invalid_country_code": "Nieznany dwuliterowy kod kraju.",
"invalid_house_number": "Nieprawid\u0142owy numer domu"
},
"step": {
@@ -8,9 +12,13 @@
"data": {
"country_code": "Dwuliterowy kod kraju",
"house_number": "Numer domu",
+ "house_number_extension": "Numer mieszkania",
"zip_code": "Kod pocztowy"
- }
+ },
+ "description": "Wpisz sw\u00f3j adres",
+ "title": "Avri"
}
}
- }
+ },
+ "title": "Avri"
}
\ No newline at end of file
diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py
index e3c2a1761196ea..b262fdec572086 100644
--- a/homeassistant/components/awair/const.py
+++ b/homeassistant/components/awair/const.py
@@ -14,6 +14,7 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
)
@@ -63,7 +64,7 @@
API_LUX: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE,
ATTR_ICON: None,
- ATTR_UNIT: "lx",
+ ATTR_UNIT: LIGHT_LUX,
ATTR_LABEL: "Illuminance",
ATTR_UNIQUE_ID: "illuminance",
},
diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json
index 1351cbd2db0611..10041e19e56942 100644
--- a/homeassistant/components/awair/strings.json
+++ b/homeassistant/components/awair/strings.json
@@ -23,7 +23,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"no_devices": "[%key:common::config_flow::abort::no_devices_found%]",
- "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}
diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json
index d0eb8e91b3e9ef..fcdcd0190e3ba2 100644
--- a/homeassistant/components/awair/translations/de.json
+++ b/homeassistant/components/awair/translations/de.json
@@ -5,7 +5,15 @@
},
"step": {
"reauth": {
+ "data": {
+ "email": "E-Mail"
+ },
"description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein."
+ },
+ "user": {
+ "data": {
+ "email": "E-Mail"
+ }
}
}
}
diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json
new file mode 100644
index 00000000000000..436e8b1fb7dd75
--- /dev/null
+++ b/homeassistant/components/awair/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json
index 07983402c42d66..76fe5a91cd92e0 100644
--- a/homeassistant/components/awair/translations/pl.json
+++ b/homeassistant/components/awair/translations/pl.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane.",
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
"no_devices": "Nie znaleziono urz\u0105dze\u0144 w sieci.",
- "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano."
+ "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano"
},
"error": {
"auth": "Token dost\u0119pu"
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index 5b300fe323bfad..ea1db54855b1ae 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -94,10 +94,10 @@ async def async_step_user(self, user_input=None):
return await self._create_entry()
except AuthenticationRequired:
- errors["base"] = "faulty_credentials"
+ errors["base"] = "invalid_auth"
except CannotConnect:
- errors["base"] = "device_unavailable"
+ errors["base"] = "cannot_connect"
data = self.discovery_schema or {
vol.Required(CONF_HOST): str,
diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json
index ceb926f326eb11..959d53a01ae35e 100644
--- a/homeassistant/components/axis/manifest.json
+++ b/homeassistant/components/axis/manifest.json
@@ -3,11 +3,11 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
- "requirements": ["axis==35"],
+ "requirements": ["axis==37"],
"zeroconf": [
- {"type":"_axis-video._tcp.local.","macaddress":"00408C*"},
- {"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"},
- {"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"}
+ { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" },
+ { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" },
+ { "type": "_axis-video._tcp.local.", "macaddress": "B8A44F*" }
],
"after_dependencies": ["mqtt"],
"codeowners": ["@Kane610"]
diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json
index 672bfe141b9d62..046b7fee475f6e 100644
--- a/homeassistant/components/axis/strings.json
+++ b/homeassistant/components/axis/strings.json
@@ -13,14 +13,13 @@
}
},
"error": {
- "already_configured": "Device is already configured",
- "already_in_progress": "Config flow for device is already in progress.",
- "device_unavailable": "Device is not available",
- "faulty_credentials": "Bad user credentials"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "Device is already configured",
- "bad_config_file": "Bad data from configuration file",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device"
}
diff --git a/homeassistant/components/axis/translations/ca.json b/homeassistant/components/axis/translations/ca.json
index 8cbbe638d24bde..695a462d4168a4 100644
--- a/homeassistant/components/axis/translations/ca.json
+++ b/homeassistant/components/axis/translations/ca.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes"
},
diff --git a/homeassistant/components/axis/translations/en.json b/homeassistant/components/axis/translations/en.json
index 29461ee0612551..bf1cdd7fda1686 100644
--- a/homeassistant/components/axis/translations/en.json
+++ b/homeassistant/components/axis/translations/en.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "Device is already configured",
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json
index 1ad2baddb85cb6..fee94009b15168 100644
--- a/homeassistant/components/axis/translations/it.json
+++ b/homeassistant/components/axis/translations/it.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"device_unavailable": "Il dispositivo non \u00e8 disponibile",
"faulty_credentials": "Credenziali utente non valide"
},
diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json
index 039e6138753838..c0b68c93d20c98 100644
--- a/homeassistant/components/axis/translations/no.json
+++ b/homeassistant/components/axis/translations/no.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"device_unavailable": "Enheten er ikke tilgjengelig",
"faulty_credentials": "Ugyldig brukerlegitimasjon"
},
diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json
index e6d55f5a5796a3..154577573bbe8b 100644
--- a/homeassistant/components/axis/translations/pl.json
+++ b/homeassistant/components/axis/translations/pl.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego",
"link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane",
"not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis"
},
"error": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne",
"faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json
index 4f8aea64e78be7..d8be3a1723cd75 100644
--- a/homeassistant/components/axis/translations/ru.json
+++ b/homeassistant/components/axis/translations/ru.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.",
"faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json
index 8e90d449f11f80..8fb6154719627a 100644
--- a/homeassistant/components/axis/translations/zh-Hant.json
+++ b/homeassistant/components/axis/translations/zh-Hant.json
@@ -8,7 +8,7 @@
},
"error": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528",
"faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548"
},
diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py
index e08dd4d8559e60..f72a4c44918d48 100644
--- a/homeassistant/components/azure_devops/__init__.py
+++ b/homeassistant/components/azure_devops/__init__.py
@@ -126,4 +126,5 @@ def device_info(self) -> Dict[str, Any]:
},
"manufacturer": self.organization,
"name": self.project,
+ "entry_type": "service",
}
diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json
index 2bb53010153ad7..64ad79c7698e7e 100644
--- a/homeassistant/components/azure_devops/strings.json
+++ b/homeassistant/components/azure_devops/strings.json
@@ -26,7 +26,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"title": "Azure DevOps"
diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json
index 7d50110a24e438..528c76767ea89d 100644
--- a/homeassistant/components/azure_devops/translations/fr.json
+++ b/homeassistant/components/azure_devops/translations/fr.json
@@ -3,6 +3,31 @@
"abort": {
"already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
"reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s"
+ },
+ "error": {
+ "authorization_error": "Erreur d'autorisation. V\u00e9rifiez que vous avez acc\u00e8s au projet et que vous disposez des informations d'identification correctes.",
+ "connection_error": "Impossible de se connecter \u00e0 Azure DevOps.",
+ "project_error": "Impossible d'obtenir les informations sur le projet."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "data": {
+ "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)"
+ },
+ "description": "L'authentification a \u00e9chou\u00e9 pour {project_url} . Veuillez saisir vos informations d'identification actuelles.",
+ "title": "R\u00e9authentification"
+ },
+ "user": {
+ "data": {
+ "organization": "Organisation",
+ "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)",
+ "project": "Projet"
+ },
+ "description": "Configurez une instance Azure DevOps pour acc\u00e9der \u00e0 votre projet. Un jeton d'acc\u00e8s personnel n'est requis que pour un projet priv\u00e9.",
+ "title": "Ajouter un projet Azure DevOps"
+ }
}
- }
+ },
+ "title": "Azure DevOps"
}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json
new file mode 100644
index 00000000000000..436e8b1fb7dd75
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/pl.json b/homeassistant/components/azure_devops/translations/pl.json
new file mode 100644
index 00000000000000..21afd65c49aab0
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/pl.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowany"
+ },
+ "error": {
+ "authorization_error": "B\u0142\u0105d autoryzacji. Sprawd\u017a, czy masz dost\u0119p do projektu i masz prawid\u0142owe po\u015bwiadczenia.",
+ "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z us\u0142ug\u0105 Azure DevOps.",
+ "project_error": "Nie mo\u017cna uzyska\u0107 informacji o projekcie."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "data": {
+ "personal_access_token": "Osobisty token dost\u0119pu (PAT)"
+ },
+ "description": "Uwierzytelnianie dla {project_url} nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.",
+ "title": "Ponowne uwierzytelnianie"
+ },
+ "user": {
+ "data": {
+ "organization": "Organizacja",
+ "personal_access_token": "Osobisty token dost\u0119pu (PAT)",
+ "project": "Projekt"
+ },
+ "description": "Skonfiguruj instancj\u0119 Azure DevOps, aby uzyska\u0107 dost\u0119p do swojego projektu. Osobisty token dost\u0119pu (PAT) jest wymagany tylko dla prywatnego projektu.",
+ "title": "Dodaj projekt Azure DevOps"
+ }
+ }
+ },
+ "title": "Azure DevOps"
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json
index 9e3f0e956e5866..d7a232d8d1ac8b 100644
--- a/homeassistant/components/azure_service_bus/manifest.json
+++ b/homeassistant/components/azure_service_bus/manifest.json
@@ -2,6 +2,6 @@
"domain": "azure_service_bus",
"name": "Azure Service Bus",
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
- "requirements": ["azure-servicebus==0.50.1"],
+ "requirements": ["azure-servicebus==0.50.3"],
"codeowners": ["@hfurubotten"]
}
diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py
index 90540e456c56ef..4768b3f4fe6261 100644
--- a/homeassistant/components/bayesian/binary_sensor.py
+++ b/homeassistant/components/bayesian/binary_sensor.py
@@ -182,6 +182,7 @@ def async_threshold_sensor_state_listener(event):
entity = event.data.get("entity_id")
self.current_observations.update(self._record_entity_observations(entity))
+ self.async_set_context(event.context)
self._recalculate_and_write_state()
self.async_on_remove(
@@ -220,6 +221,8 @@ def _async_template_result_changed(event, updates):
obs_entry = None
self.current_observations[obs["id"]] = obs_entry
+ if event:
+ self.async_set_context(event.context)
self._recalculate_and_write_state()
for template in self.observations_by_template:
diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py
index df8e87f751d179..4d4067eb77deca 100644
--- a/homeassistant/components/bh1750/sensor.py
+++ b/homeassistant/components/bh1750/sensor.py
@@ -7,7 +7,7 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE
+from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -37,7 +37,6 @@
ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution.
}
-SENSOR_UNIT = "lx"
DEFAULT_NAME = "BH1750 Light Sensor"
DEFAULT_I2C_ADDRESS = "0x23"
DEFAULT_I2C_BUS = 1
@@ -85,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
_LOGGER.error("BH1750 sensor not detected at %s", i2c_address)
return False
- dev = [BH1750Sensor(sensor, name, SENSOR_UNIT, config[CONF_MULTIPLIER])]
+ 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,
diff --git a/homeassistant/components/binary_sensor/group.py b/homeassistant/components/binary_sensor/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/binary_sensor/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json
index a9da1be9ee2888..c074c56675ac2e 100644
--- a/homeassistant/components/binary_sensor/translations/et.json
+++ b/homeassistant/components/binary_sensor/translations/et.json
@@ -1,4 +1,94 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} aku on t\u00fchjenemas",
+ "is_cold": "{entity_name} on k\u00fclm",
+ "is_connected": "{entity_name} on \u00fchendatud",
+ "is_gas": "{entity_name} tuvastab gaasi(leket)",
+ "is_hot": "{entity_name} on kuum",
+ "is_light": "{entity_name} tuvastab valgust",
+ "is_locked": "{entity_name} on lukustatud",
+ "is_moist": "{entity_name} on niiske",
+ "is_motion": "{entity_name} tuvastab liikumist",
+ "is_moving": "{entity_name} liigub",
+ "is_no_gas": "{entity_name} ei tuvasta gaasi(leket)",
+ "is_no_light": "{entity_name} ei tuvasta valgust",
+ "is_no_motion": "{entity_name} ei tuvasta liikumist",
+ "is_no_problem": "{entity_name} ei leia probleemi",
+ "is_no_smoke": "{entity_name} ei tuvasta suitsu",
+ "is_no_sound": "{entity_name} ei tuvasta heli",
+ "is_no_vibration": "{entity_name} ei tuvasta vibratsiooni",
+ "is_not_bat_low": "{entity_name} aku on laetud",
+ "is_not_cold": "{entity_name} ei ole k\u00fclm",
+ "is_not_connected": "{entity_name} pole \u00fchendatud",
+ "is_not_hot": "{entity_name} ei ole kuum",
+ "is_not_locked": "{entity_name} on lukustamata",
+ "is_not_moist": "{entity_name} on kuiv",
+ "is_not_moving": "{entity_name} liikumist ei tuvastatud",
+ "is_not_occupied": "{entity_name} pole h\u00f5ivatud",
+ "is_not_open": "{entity_name} on suletud",
+ "is_not_plugged_in": "{entity_name} on lahti \u00fchendatud",
+ "is_not_powered": "{entity_name} ei ole voolu all",
+ "is_not_present": "{entity_name} puudub",
+ "is_not_unsafe": "{entity_name} on turvaline",
+ "is_occupied": "{entity_name} on h\u00f5ivatud",
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud",
+ "is_open": "{entity_name} on avatud",
+ "is_plugged_in": "{entity_name} on \u00fchendatud",
+ "is_powered": "{entity_name} on voolu all",
+ "is_present": "{entity_name} on saadaval",
+ "is_problem": "Olemil {entity_name} on probleem",
+ "is_smoke": "{entity_name} tuvastab suitsu",
+ "is_sound": "{entity_name} tuvastab heli",
+ "is_unsafe": "{entity_name} on ebaturvaline",
+ "is_vibration": "{entity_name} tuvastab vibratsiooni"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} aku hakkab t\u00fchjaks saama",
+ "cold": "{entity_name} muutus k\u00fclmaks",
+ "connected": "{entity_name} on \u00fchendatud",
+ "gas": "{entity_name} tuvastas gaasi(leket)",
+ "hot": "{entity_name} muutus kuumaks",
+ "light": "{entity_name} tuvastas valgust",
+ "locked": "{entity_name} on lukus",
+ "moist": "{entity_name} muutus niiskeks",
+ "motion": "{entity_name} tuvastas liikumist",
+ "moving": "{entity_name} hakkas liikuma",
+ "no_gas": "{entity_name} l\u00f5petas gaasi(lekke) tuvastamise",
+ "no_light": "{entity_name} l\u00f5petas valguse tuvastamise",
+ "no_motion": "{entity_name} l\u00f5petas liikumise tuvastamise",
+ "no_problem": "{entity_name} l\u00f5petas probleemi tuvastamise",
+ "no_smoke": "{entity_name} l\u00f5petas suitsu tuvastamise",
+ "no_sound": "{entity_name} l\u00f5petas heli tuvastamise",
+ "no_vibration": "{entity_name} l\u00f5petas vibratsiooni tuvastamise",
+ "not_bat_low": "{entity_name} aku on laetud",
+ "not_cold": "{entity_name} ei ole enam k\u00fclm",
+ "not_connected": "{entity_name} on lahti \u00fchendatud",
+ "not_hot": "{entity_name} ei ole enam kuum",
+ "not_locked": "{entity_name} on lukustamata",
+ "not_moist": "{entity_name} muutus kuivaks",
+ "not_moving": "{entity_name} liikumine peatus",
+ "not_occupied": "{entity_name} vabanes h\u00f5ivest",
+ "not_opened": "{entity_name} sulgus",
+ "not_plugged_in": "{entity_name} \u00fchendati vooluv\u00f5rgust v\u00e4lja",
+ "not_powered": "{entity_name} pole toidet",
+ "not_present": "{entity_name} puudub",
+ "not_unsafe": "{entity_name} muutus turvaliseks",
+ "occupied": "{entity_name} h\u00f5ivati",
+ "opened": "{entity_name} avanes",
+ "plugged_in": "{entity_name} \u00fchendati",
+ "powered": "{entity_name} l\u00fcltus voolu alla",
+ "present": "{entity_name} on saadaval",
+ "problem": "{entity_name} avastas probleemi",
+ "smoke": "{entity_name} tuvastas suitsu",
+ "sound": "{entity_name} tuvastas heli",
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse",
+ "unsafe": "{entity_name} on ebaturvaline",
+ "vibration": "{entity_name} registreeris vibratsiooni"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
@@ -41,8 +131,8 @@
"on": "M\u00e4rg"
},
"motion": {
- "off": "Puudub",
- "on": "Tuvastatud"
+ "off": "Liikumine puudub",
+ "on": "Liikumine tuvastatud"
},
"occupancy": {
"off": "Puudub",
diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json
index 7b01acae4fb55b..29767f6d6d62e5 100644
--- a/homeassistant/components/binary_sensor/translations/uk.json
+++ b/homeassistant/components/binary_sensor/translations/uk.json
@@ -1,4 +1,18 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430",
+ "is_not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u043c \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430",
+ "not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440",
+ "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e",
+ "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e",
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json
index f929d62d8d90c0..9a3c261a34a1b8 100644
--- a/homeassistant/components/blebox/strings.json
+++ b/homeassistant/components/blebox/strings.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "This BleBox device is already configured.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"address_already_configured": "A BleBox device is already configured at {address}."
},
"error": {
- "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.",
- "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "BleBox device: {name} ({host})",
"step": {
diff --git a/homeassistant/components/blebox/translations/ca.json b/homeassistant/components/blebox/translations/ca.json
index 39bf371fac1dd4..aba528fafb57d0 100644
--- a/homeassistant/components/blebox/translations/ca.json
+++ b/homeassistant/components/blebox/translations/ca.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "Ja hi ha un dispositiu BleBox configurat a {address}.",
- "already_configured": "Aquest dispositiu BleBox ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar al dispositiu BleBox. (Consulta els registres per veure-hi els errors).",
- "unknown": "S'ha produ\u00eft un error desconegut en connectar-se al dispositiu BleBox. (Consulta els registres per veure-hi els errors).",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "unknown": "Error inesperat",
"unsupported_version": "El dispositiu BleBox t\u00e9 un firmware obsolet. Primer actualitza'l."
},
"flow_title": "Dispositiu BleBox: {name} ({host})",
diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json
index 29f7a03bb3145d..7ff38e25343279 100644
--- a/homeassistant/components/blebox/translations/en.json
+++ b/homeassistant/components/blebox/translations/en.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "A BleBox device is already configured at {address}.",
- "already_configured": "This BleBox device is already configured."
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)",
- "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)",
+ "cannot_connect": "Failed to connect",
+ "unknown": "Unexpected error",
"unsupported_version": "BleBox device has outdated firmware. Please upgrade it first."
},
"flow_title": "BleBox device: {name} ({host})",
diff --git a/homeassistant/components/blebox/translations/fr.json b/homeassistant/components/blebox/translations/fr.json
index 75d506a82128e4..d30d026d177dca 100644
--- a/homeassistant/components/blebox/translations/fr.json
+++ b/homeassistant/components/blebox/translations/fr.json
@@ -6,7 +6,8 @@
},
"error": {
"cannot_connect": "Impossible de connecter le p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)",
- "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)"
+ "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)",
+ "unsupported_version": "L'appareil BleBox a un micrologiciel obsol\u00e8te. Veuillez d'abord le mettre \u00e0 jour."
},
"flow_title": "P\u00e9riph\u00e9rique Blebox: {name} ({host)}",
"step": {
diff --git a/homeassistant/components/blebox/translations/it.json b/homeassistant/components/blebox/translations/it.json
index 73f7b9276e92bc..265a158e22d628 100644
--- a/homeassistant/components/blebox/translations/it.json
+++ b/homeassistant/components/blebox/translations/it.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "Un dispositivo BleBox \u00e8 gi\u00e0 configurato in {address}.",
- "already_configured": "Questo dispositivo BleBox \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi al dispositivo BleBox. (Controllare i registri per errori).",
- "unknown": "Errore sconosciuto durante la connessione al dispositivo BleBox. (Controllare i registri per errori).",
+ "cannot_connect": "Impossibile connettersi",
+ "unknown": "Errore imprevisto",
"unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Si prega di aggiornarlo prima."
},
"flow_title": "Dispositivo BleBox: {name} ({host})",
diff --git a/homeassistant/components/blebox/translations/no.json b/homeassistant/components/blebox/translations/no.json
index 239d1fb03c6af8..7bede9535e79af 100644
--- a/homeassistant/components/blebox/translations/no.json
+++ b/homeassistant/components/blebox/translations/no.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "En BleBox-enhet er allerede konfigurert p\u00e5 {address} .",
- "already_configured": "Denne BleBox-enheten er allerede konfigurert."
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Kan ikke koble til BleBox-enheten. (Kontroller loggene for feil.)",
- "unknown": "Ukjent feil under tilkobling til BleBox-enheten. (Kontroller loggene for feil.)",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "unknown": "Uventet feil",
"unsupported_version": "BleBox-enheten har utdatert fastvare. Vennligst oppgrader den f\u00f8rst."
},
"flow_title": "BleBox-enhet: {name} ({host})",
diff --git a/homeassistant/components/blebox/translations/ru.json b/homeassistant/components/blebox/translations/ru.json
index b82261be7f768b..4fd361021ebd57 100644
--- a/homeassistant/components/blebox/translations/ru.json
+++ b/homeassistant/components/blebox/translations/ru.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {address } \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.",
- "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"unsupported_version": "\u041f\u0440\u043e\u0448\u0438\u0432\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0435\u0451."
},
"flow_title": "BleBox device: {name} ({host})",
diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py
index 35cc2d0d5a5d0e..3c3adf6d990e6d 100644
--- a/homeassistant/components/blink/sensor.py
+++ b/homeassistant/components/blink/sensor.py
@@ -4,6 +4,7 @@
from homeassistant.const import (
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers.entity import Entity
@@ -14,7 +15,11 @@
SENSORS = {
TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE],
- TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", DEVICE_CLASS_SIGNAL_STRENGTH],
+ TYPE_WIFI_STRENGTH: [
+ "Wifi Signal",
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ ],
}
diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json
index ef3ffc108e57b3..ac8c96e4f2d7a5 100644
--- a/homeassistant/components/blink/translations/ko.json
+++ b/homeassistant/components/blink/translations/ko.json
@@ -4,6 +4,8 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_access_token": "\uc798\ubabb\ub41c \uc778\uc99d",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json
index 7d6d01266d97a6..50436c77e824ca 100644
--- a/homeassistant/components/blink/translations/pl.json
+++ b/homeassistant/components/blink/translations/pl.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_access_token": "Niepoprawny token dost\u0119pu",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"2fa": {
@@ -23,5 +25,16 @@
"title": "Zaloguj si\u0119 za pomoc\u0105 konta Blink"
}
}
+ },
+ "options": {
+ "step": {
+ "simple_options": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)"
+ },
+ "description": "Konfigurowanie integracji Blink",
+ "title": "Opcje Blink"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py
index 56009c90da3731..71e6a6b2c71786 100644
--- a/homeassistant/components/blinksticklight/light.py
+++ b/homeassistant/components/blinksticklight/light.py
@@ -54,11 +54,6 @@ def __init__(self, stick, name):
self._hs_color = None
self._brightness = None
- @property
- def should_poll(self):
- """Set up polling."""
- return True
-
@property
def name(self):
"""Return the name of the light."""
diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py
index 929f8218144ae9..cd993e0332a366 100644
--- a/homeassistant/components/bloomsky/__init__.py
+++ b/homeassistant/components/bloomsky/__init__.py
@@ -6,7 +6,12 @@
import requests
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY, HTTP_OK
+from homeassistant.const import (
+ CONF_API_KEY,
+ HTTP_METHOD_NOT_ALLOWED,
+ HTTP_OK,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@@ -67,9 +72,9 @@ def refresh_devices(self):
headers={AUTHORIZATION: self._api_key},
timeout=10,
)
- if response.status_code == 401:
+ if response.status_code == HTTP_UNAUTHORIZED:
raise RuntimeError("Invalid API_KEY")
- if response.status_code == 405:
+ if response.status_code == HTTP_METHOD_NOT_ALLOWED:
_LOGGER.error("You have no bloomsky devices configured")
return
if response.status_code != HTTP_OK:
diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py
index 077171006bf807..43c5614679c324 100644
--- a/homeassistant/components/bloomsky/binary_sensor.py
+++ b/homeassistant/components/bloomsky/binary_sensor.py
@@ -3,7 +3,11 @@
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
@@ -11,7 +15,7 @@
_LOGGER = logging.getLogger(__name__)
-SENSOR_TYPES = {"Rain": "moisture", "Night": None}
+SENSOR_TYPES = {"Rain": DEVICE_CLASS_MOISTURE, "Night": None}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py
index 0ddeec6a5773a3..eaa03ef2f3b0eb 100644
--- a/homeassistant/components/bloomsky/sensor.py
+++ b/homeassistant/components/bloomsky/sensor.py
@@ -5,8 +5,11 @@
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
+ AREA_SQUARE_METERS,
CONF_MONITORED_CONDITIONS,
PERCENTAGE,
+ PRESSURE_INHG,
+ PRESSURE_MBAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
@@ -31,8 +34,8 @@
SENSOR_UNITS_IMPERIAL = {
"Temperature": TEMP_FAHRENHEIT,
"Humidity": PERCENTAGE,
- "Pressure": "inHg",
- "Luminance": "cd/m²",
+ "Pressure": PRESSURE_INHG,
+ "Luminance": f"cd/{AREA_SQUARE_METERS}",
"Voltage": "mV",
}
@@ -40,8 +43,8 @@
SENSOR_UNITS_METRIC = {
"Temperature": TEMP_CELSIUS,
"Humidity": PERCENTAGE,
- "Pressure": "mbar",
- "Luminance": "cd/m²",
+ "Pressure": PRESSURE_MBAR,
+ "Luminance": f"cd/{AREA_SQUARE_METERS}",
"Voltage": "mV",
}
diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py
index ee89873e8feff0..31ef2dacf3aa21 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -3,7 +3,12 @@
from bimmer_connected.state import ChargingState, LockState
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_PLUG,
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS
from . import DOMAIN as BMW_DOMAIN
@@ -12,17 +17,25 @@
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
- "lids": ["Doors", "opening", "mdi:car-door-lock"],
- "windows": ["Windows", "opening", "mdi:car-door"],
+ "lids": ["Doors", DEVICE_CLASS_OPENING, "mdi:car-door-lock"],
+ "windows": ["Windows", DEVICE_CLASS_OPENING, "mdi:car-door"],
"door_lock_state": ["Door lock state", "lock", "mdi:car-key"],
"lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"],
- "condition_based_services": ["Condition based services", "problem", "mdi:wrench"],
- "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"],
+ "condition_based_services": [
+ "Condition based services",
+ DEVICE_CLASS_PROBLEM,
+ "mdi:wrench",
+ ],
+ "check_control_messages": [
+ "Control messages",
+ DEVICE_CLASS_PROBLEM,
+ "mdi:car-tire-alert",
+ ],
}
SENSOR_TYPES_ELEC = {
"charging_status": ["Charging status", "power", "mdi:ev-station"],
- "connection_status": ["Connection status", "plug", "mdi:car-electric"],
+ "connection_status": ["Connection status", DEVICE_CLASS_PLUG, "mdi:car-electric"],
}
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py
index 56406b29e82b20..0470ea6e9704f0 100644
--- a/homeassistant/components/bom/sensor.py
+++ b/homeassistant/components/bom/sensor.py
@@ -21,7 +21,9 @@
CONF_NAME,
LENGTH_KILOMETERS,
LENGTH_METERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
+ PRESSURE_MBAR,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
)
@@ -66,11 +68,11 @@
"gust_kt": ["Wind Gust kt", "kt"],
"air_temp": ["Air Temp C", TEMP_CELSIUS],
"dewpt": ["Dew Point C", TEMP_CELSIUS],
- "press": ["Pressure mb", "mbar"],
+ "press": ["Pressure mb", PRESSURE_MBAR],
"press_qnh": ["Pressure qnh", "qnh"],
"press_msl": ["Pressure msl", "msl"],
"press_tend": ["Pressure Tend", None],
- "rain_trace": ["Rain Today", "mm"],
+ "rain_trace": ["Rain Today", LENGTH_MILLIMETERS],
"rel_hum": ["Relative Humidity", PERCENTAGE],
"sea_state": ["Sea State", None],
"swell_dir_worded": ["Swell Direction", None],
@@ -173,7 +175,7 @@ def state(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
- attr = {
+ return {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_LAST_UPDATE: self.bom_data.last_updated,
ATTR_SENSOR_ID: self._condition,
@@ -182,8 +184,6 @@ def device_state_attributes(self):
ATTR_ZONE_ID: self.bom_data.latest_data["history_product"],
}
- return attr
-
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
diff --git a/homeassistant/components/bom/weather.py b/homeassistant/components/bom/weather.py
index 94b9960c851bce..9229d0c11d4356 100644
--- a/homeassistant/components/bom/weather.py
+++ b/homeassistant/components/bom/weather.py
@@ -54,7 +54,9 @@ def name(self):
@property
def condition(self):
"""Return the current condition."""
- return self.bom_data.get_reading("weather")
+ return self.bom_data.get_reading("weather") or self.bom_data.get_reading(
+ "cloud"
+ )
# Now implement the WeatherEntity interface
diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py
index 49ca559685c141..6666cd57ca357e 100644
--- a/homeassistant/components/bond/config_flow.py
+++ b/homeassistant/components/bond/config_flow.py
@@ -7,7 +7,12 @@
import voluptuous as vol
from homeassistant import config_entries, exceptions
-from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_HOST,
+ CONF_NAME,
+ HTTP_UNAUTHORIZED,
+)
from .const import CONF_BOND_ID
from .const import DOMAIN # pylint:disable=unused-import
@@ -31,7 +36,7 @@ async def _validate_input(data: Dict[str, Any]) -> str:
except ClientConnectionError as error:
raise InputValidationError("cannot_connect") from error
except ClientResponseError as error:
- if error.status == 401:
+ if error.status == HTTP_UNAUTHORIZED:
raise InputValidationError("invalid_auth") from error
raise InputValidationError("unknown") from error
except Exception as error:
diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py
index 19e345b7e23092..e59d0234beb8ed 100644
--- a/homeassistant/components/bond/fan.py
+++ b/homeassistant/components/bond/fan.py
@@ -120,7 +120,7 @@ async def async_set_speed(self, speed: str) -> None:
async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
"""Turn on the fan."""
- _LOGGER.debug("async_turn_on called with speed %s", speed)
+ _LOGGER.debug("Fan async_turn_on called with speed %s", speed)
if speed is not None:
if speed == SPEED_OFF:
diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py
index 7dec44dbb38a7a..5e66019579dce8 100644
--- a/homeassistant/components/bond/light.py
+++ b/homeassistant/components/bond/light.py
@@ -1,4 +1,5 @@
"""Support for Bond lights."""
+import logging
from typing import Any, Callable, List, Optional
from bond_api import Action, DeviceType
@@ -17,6 +18,8 @@
from .entity import BondEntity
from .utils import BondDevice
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup_entry(
hass: HomeAssistant,
@@ -96,7 +99,7 @@ class BondFireplace(BondEntity, LightEntity):
"""Representation of a Bond-controlled fireplace."""
def __init__(self, hub: BondHub, device: BondDevice):
- """Create HA entity representing Bond fan."""
+ """Create HA entity representing Bond fireplace."""
super().__init__(hub, device)
self._power: Optional[bool] = None
@@ -119,6 +122,8 @@ def is_on(self) -> bool:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the fireplace on."""
+ _LOGGER.debug("Fireplace async_turn_on called with: %s", kwargs)
+
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness:
flame = round((brightness * 100) / 255)
@@ -128,6 +133,8 @@ async def async_turn_on(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fireplace off."""
+ _LOGGER.debug("Fireplace async_turn_off called with: %s", kwargs)
+
await self._hub.bond.action(self._device.device_id, Action.turn_off())
@property
diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json
index d10ea1e71b00ca..393232025ddc1f 100644
--- a/homeassistant/components/bond/translations/de.json
+++ b/homeassistant/components/bond/translations/de.json
@@ -7,7 +7,8 @@
"step": {
"user": {
"data": {
- "access_token": "Zugriffstoken"
+ "access_token": "Zugriffstoken",
+ "host": "Host"
}
}
}
diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json
index cabbb73a370440..496a21339cbf24 100644
--- a/homeassistant/components/bond/translations/fr.json
+++ b/homeassistant/components/bond/translations/fr.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Echec de connexion",
"invalid_auth": "Authentification invalide",
+ "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer",
"unknown": "Erreur inattendue"
},
"flow_title": "Bond : {bond_id} ({h\u00f4te})",
diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/bond/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json
index d50380c81ebef5..61576d7043166b 100644
--- a/homeassistant/components/bond/translations/ko.json
+++ b/homeassistant/components/bond/translations/ko.json
@@ -5,7 +5,11 @@
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "\ubcf8\ub4dc : {bond_id} ( {host} )",
"step": {
+ "confirm": {
+ "description": "{bond_id} \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ },
"user": {
"data": {
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070",
diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json
index 10b6433daee865..c50c270b74cd50 100644
--- a/homeassistant/components/bond/translations/pl.json
+++ b/homeassistant/components/bond/translations/pl.json
@@ -1,11 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "old_firmware": "Stare, nieobs\u0142ugiwane oprogramowanie na urz\u0105dzeniu Bond - zaktualizuj przed kontynuowaniem",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
+ "flow_title": "Bond: {bond_id} ({host})",
"step": {
+ "confirm": {
+ "data": {
+ "access_token": "Token dost\u0119pu"
+ },
+ "description": "Czy chcesz skonfigurowa\u0107 {bond_id}?"
+ },
"user": {
"data": {
"access_token": "Token dost\u0119pu",
diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json
index c066f91d395b00..7414b8008c15f3 100644
--- a/homeassistant/components/braviatv/strings.json
+++ b/homeassistant/components/braviatv/strings.json
@@ -12,17 +12,17 @@
"title": "Authorize Sony Bravia TV",
"description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.",
"data": {
- "pin": "PIN code"
+ "pin": "[%key:common::config_flow::data::pin%]"
}
}
},
"error": {
"invalid_host": "Invalid hostname or IP address.",
- "cannot_connect": "Failed to connect, invalid host or PIN code.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unsupported_model": "Your TV model is not supported."
},
"abort": {
- "already_configured": "This TV is already configured.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_ip_control": "IP Control is disabled on your TV or the TV is not supported."
}
},
@@ -36,4 +36,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json
index c35dc3ce8576cd..c46036e428b09b 100644
--- a/homeassistant/components/braviatv/translations/ca.json
+++ b/homeassistant/components/braviatv/translations/ca.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Aquest televisor ja est\u00e0 configurat.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible."
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, amfitri\u00f3 o codi PIN inv\u00e0lids.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids.",
"unsupported_model": "Aquest model de televisor no \u00e9s compatible."
},
diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json
index 98a569b3ab1290..d32b674a7a1c3c 100644
--- a/homeassistant/components/braviatv/translations/en.json
+++ b/homeassistant/components/braviatv/translations/en.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "This TV is already configured.",
+ "already_configured": "Device is already configured",
"no_ip_control": "IP Control is disabled on your TV or the TV is not supported."
},
"error": {
- "cannot_connect": "Failed to connect, invalid host or PIN code.",
+ "cannot_connect": "Failed to connect",
"invalid_host": "Invalid hostname or IP address.",
"unsupported_model": "Your TV model is not supported."
},
diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json
index 46cb5fca7a4c4e..5df455386a3c22 100644
--- a/homeassistant/components/braviatv/translations/it.json
+++ b/homeassistant/components/braviatv/translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Questo televisore \u00e8 gi\u00e0 configurato.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata."
},
"error": {
- "cannot_connect": "Connessione non riuscita, host o codice PIN non valido.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_host": "Nome host o indirizzo IP non valido.",
"unsupported_model": "Il tuo modello TV non \u00e8 supportato."
},
diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json
index cd687d8f2d0cc9..a890890c072b43 100644
--- a/homeassistant/components/braviatv/translations/no.json
+++ b/homeassistant/components/braviatv/translations/no.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Denne TV-en er allerede konfigurert.",
+ "already_configured": "Enheten er allerede konfigurert",
"no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke."
},
"error": {
- "cannot_connect": "Kunne ikke koble til, ugyldig vert eller PIN-kode.",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_host": "Ugyldig vertsnavn eller IP-adresse.",
"unsupported_model": "TV-modellen din st\u00f8ttes ikke."
},
diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json
index a799a29831fe58..add32dd0d794a5 100644
--- a/homeassistant/components/braviatv/translations/ru.json
+++ b/homeassistant/components/braviatv/translations/ru.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
"no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 PIN-\u043a\u043e\u0434.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.",
"unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f."
},
diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json
index 027e44abaa4754..d45b1b778626ba 100644
--- a/homeassistant/components/braviatv/translations/zh-Hant.json
+++ b/homeassistant/components/braviatv/translations/zh-Hant.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u7121\u6548\u7684\u4e3b\u6a5f\u540d\u7a31\u6216 PIN \u78bc\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740",
"unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u578b\u865f\u3002"
},
diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py
index 284a9bffe19c1d..9fe83e350cfbd5 100644
--- a/homeassistant/components/broadlink/config_flow.py
+++ b/homeassistant/components/broadlink/config_flow.py
@@ -7,11 +7,11 @@
from broadlink.exceptions import (
AuthenticationError,
BroadlinkException,
- DeviceOfflineError,
+ NetworkTimeoutError,
)
import voluptuous as vol
-from homeassistant import config_entries
+from homeassistant import config_entries, data_entry_flow
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
from homeassistant.helpers import config_validation as cv
@@ -20,6 +20,7 @@
DEFAULT_PORT,
DEFAULT_TIMEOUT,
DOMAIN,
+ DOMAINS_AND_TYPES,
)
from .helpers import format_mac
@@ -36,6 +37,19 @@ def __init__(self):
async def async_set_device(self, device, raise_on_progress=True):
"""Define a device for the config flow."""
+ supported_types = {
+ device_type
+ for _, device_types in DOMAINS_AND_TYPES
+ for device_type in device_types
+ }
+ if device.type not in supported_types:
+ LOGGER.error(
+ "Unsupported device: %s. If it worked before, please open "
+ "an issue at https://github.com/home-assistant/core/issues",
+ hex(device.devtype),
+ )
+ raise data_entry_flow.AbortFlow("not_supported")
+
await self.async_set_unique_id(
device.mac.hex(), raise_on_progress=raise_on_progress
)
@@ -125,7 +139,7 @@ async def async_step_auth(self):
await self.async_set_unique_id(device.mac.hex())
return await self.async_step_reset(errors=errors)
- except DeviceOfflineError as err:
+ except NetworkTimeoutError as err:
errors["base"] = "cannot_connect"
err_msg = str(err)
@@ -193,7 +207,7 @@ async def async_step_unlock(self, user_input=None):
try:
await self.hass.async_add_executor_job(device.set_lock, False)
- except DeviceOfflineError as err:
+ except NetworkTimeoutError as err:
errors["base"] = "cannot_connect"
err_msg = str(err)
diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py
index d05fdfd4df6406..51d9b0a497ffa2 100644
--- a/homeassistant/components/broadlink/device.py
+++ b/homeassistant/components/broadlink/device.py
@@ -9,7 +9,7 @@
AuthorizationError,
BroadlinkException,
ConnectionClosedError,
- DeviceOfflineError,
+ NetworkTimeoutError,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
@@ -82,7 +82,7 @@ async def async_setup(self):
await self._async_handle_auth_error()
return False
- except (DeviceOfflineError, OSError) as err:
+ except (NetworkTimeoutError, OSError) as err:
raise ConfigEntryNotReady from err
except BroadlinkException as err:
diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json
index 6a6300449044cd..9c6e571ec8685e 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.14.1"],
+ "requirements": ["broadlink==0.15.0"],
"codeowners": ["@danielhiversen", "@felipediel"],
"config_flow": true
}
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index 7c1ae7349da85b..7c7a05ac1676b7 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -9,7 +9,7 @@
from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
- DeviceOfflineError,
+ NetworkTimeoutError,
ReadError,
StorageError,
)
@@ -243,6 +243,9 @@ async def async_send_command(self, command, **kwargs):
delay = kwargs[ATTR_DELAY_SECS]
if not self._state:
+ _LOGGER.warning(
+ "remote.send_command canceled: %s entity is turned off", self.entity_id
+ )
return
should_delay = False
@@ -262,7 +265,7 @@ async def async_send_command(self, command, **kwargs):
try:
await self._device.async_request(self._device.api.send_data, code)
- except (AuthorizationError, DeviceOfflineError, OSError) as err:
+ except (AuthorizationError, NetworkTimeoutError, OSError) as err:
_LOGGER.error("Failed to send '%s': %s", command, err)
break
@@ -285,6 +288,9 @@ async def async_learn_command(self, **kwargs):
toggle = kwargs[ATTR_ALTERNATIVE]
if not self._state:
+ _LOGGER.warning(
+ "remote.learn_command canceled: %s entity is turned off", self.entity_id
+ )
return
should_store = False
@@ -295,7 +301,7 @@ async def async_learn_command(self, **kwargs):
if toggle:
code = [code, await self._async_learn_command(command)]
- except (AuthorizationError, DeviceOfflineError, OSError) as err:
+ except (AuthorizationError, NetworkTimeoutError, OSError) as err:
_LOGGER.error("Failed to learn '%s': %s", command, err)
break
diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json
index 44cb1801eded1d..efa9d3c35f072a 100644
--- a/homeassistant/components/broadlink/strings.json
+++ b/homeassistant/components/broadlink/strings.json
@@ -26,15 +26,16 @@
"finish": {
"title": "Choose a name for the device",
"data": {
- "name": "Name"
+ "name": "[%key:common::config_flow::data::name%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "There is already a configuration flow in progress for this device",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_host": "Invalid hostname or IP address",
+ "not_supported": "Device not supported",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json
index 3e642b0f6b5e75..edde3bbdfcf8a5 100644
--- a/homeassistant/components/broadlink/translations/ca.json
+++ b/homeassistant/components/broadlink/translations/ca.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "Ja hi ha un flux de configuraci\u00f3 en curs per a aquest dispositiu",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids",
+ "not_supported": "Dispositiu no compatible",
"unknown": "Error inesperat"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json
new file mode 100644
index 00000000000000..b0d7a55c78774e
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/de.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "finish": {
+ "data": {
+ "name": "Name"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/el.json b/homeassistant/components/broadlink/translations/el.json
new file mode 100644
index 00000000000000..af16604830e394
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/el.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "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",
+ "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"
+ },
+ "error": {
+ "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"
+ },
+ "flow_title": "{name} ( {model} \u03c3\u03c4\u03bf {host} )",
+ "step": {
+ "auth": {
+ "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
+ },
+ "finish": {
+ "data": {
+ "name": "\u038c\u03bd\u03bf\u03bc\u03b1"
+ },
+ "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.",
+ "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;",
+ "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": {
+ "data": {
+ "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf"
+ },
+ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/en.json b/homeassistant/components/broadlink/translations/en.json
index fa3feb880080b7..0b1029b8a0a3c2 100644
--- a/homeassistant/components/broadlink/translations/en.json
+++ b/homeassistant/components/broadlink/translations/en.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "There is already a configuration flow in progress for this device",
+ "already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect",
"invalid_host": "Invalid hostname or IP address",
+ "not_supported": "Device not supported",
"unknown": "Unexpected error"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json
index fdceeabd2cd414..98c4cbdfb30486 100644
--- a/homeassistant/components/broadlink/translations/es.json
+++ b/homeassistant/components/broadlink/translations/es.json
@@ -5,6 +5,7 @@
"already_in_progress": "Ya hay un flujo de configuraci\u00f3n en curso para este dispositivo",
"cannot_connect": "No se pudo conectar",
"invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos",
+ "not_supported": "Dispositivo no compatible",
"unknown": "Error inesperado"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/et.json b/homeassistant/components/broadlink/translations/et.json
new file mode 100644
index 00000000000000..fc7f3424b6286a
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "not_supported": "Seadet ei toetata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json
index 2bf2477f61522d..1d80059fb7a142 100644
--- a/homeassistant/components/broadlink/translations/fr.json
+++ b/homeassistant/components/broadlink/translations/fr.json
@@ -5,6 +5,7 @@
"already_in_progress": "Il y a d\u00e9j\u00e0 un processus de configuration en cours pour cet appareil",
"cannot_connect": "\u00c9chec de connexion",
"invalid_host": "Nom d'h\u00f4te ou adresse IP non valide",
+ "not_supported": "Dispositif non pris en charge",
"unknown": "Erreur inattendue"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/it.json b/homeassistant/components/broadlink/translations/it.json
index 939925104fd080..d8e64b4bea33e5 100644
--- a/homeassistant/components/broadlink/translations/it.json
+++ b/homeassistant/components/broadlink/translations/it.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "\u00c8 gi\u00e0 in corso un flusso di configurazione per questo dispositivo",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"cannot_connect": "Impossibile connettersi",
"invalid_host": "Nome host o indirizzo IP non valido",
+ "not_supported": "Dispositivo non supportato",
"unknown": "Errore imprevisto"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json
new file mode 100644
index 00000000000000..47ebf3db64a08a
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/ko.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "already_in_progress": "\uc774 \uae30\uae30\uc5d0 \ub300\ud574 \uc774\ubbf8 \uc9c4\ud589\uc911\uc778 \uad6c\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c",
+ "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "flow_title": "{name} ({host} \uc758 {model})",
+ "step": {
+ "auth": {
+ "title": "\uc7a5\uce58\uc5d0 \uc778\uc99d"
+ },
+ "finish": {
+ "title": "\uc7a5\uce58 \uc774\ub984\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624"
+ },
+ "reset": {
+ "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "\uc608"
+ },
+ "description": "\uc7a5\uce58\uac00 \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c (\uc635\uc158)"
+ },
+ "user": {
+ "data": {
+ "timeout": "\uc81c\ud55c \uc2dc\uac04"
+ },
+ "title": "\uc7a5\uce58\uc5d0 \uc5f0\uacb0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/lb.json b/homeassistant/components/broadlink/translations/lb.json
index 0872c01b608845..e4f60a3eac9457 100644
--- a/homeassistant/components/broadlink/translations/lb.json
+++ b/homeassistant/components/broadlink/translations/lb.json
@@ -5,6 +5,7 @@
"already_in_progress": "Et ass schonn ee Konfiguratioun's Oflaf fir d\u00ebsen Apparat am gaang.",
"cannot_connect": "Feeler beim verbannen",
"invalid_host": "Ong\u00ebltege Numm oder IP Adresse.",
+ "not_supported": "Apparat net \u00ebnnerst\u00ebtzt.",
"unknown": "Onerwaarte Feeler"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json
new file mode 100644
index 00000000000000..d6185150e499f6
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/nl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "not_supported": "Apparaat wordt niet ondersteund"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/no.json b/homeassistant/components/broadlink/translations/no.json
index beeae80745d527..ad07b2b6d95841 100644
--- a/homeassistant/components/broadlink/translations/no.json
+++ b/homeassistant/components/broadlink/translations/no.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Det p\u00e5g\u00e5r allerede en konfigurasjonsflyt for denne enheten",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"cannot_connect": "Tilkobling mislyktes.",
"invalid_host": "Ugyldig vertsnavn eller IP-adresse",
+ "not_supported": "Enheten st\u00f8ttes ikke",
"unknown": "Uventet feil"
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/pl.json b/homeassistant/components/broadlink/translations/pl.json
index a168d020d02983..8c4704475917db 100644
--- a/homeassistant/components/broadlink/translations/pl.json
+++ b/homeassistant/components/broadlink/translations/pl.json
@@ -5,6 +5,7 @@
"already_in_progress": "Konfiguracja integracji Broadlink jest ju\u017c w toku.",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem",
"invalid_host": "B\u0142\u0119dna nazwa hosta b\u0105d\u017a adres IP",
+ "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane",
"unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d"
},
"error": {
@@ -24,7 +25,7 @@
"title": "Wprowad\u017a nazw\u0119 dla urz\u0105dzenia"
},
"reset": {
- "description": "Twoje urz\u0105dzenie jest zablokowane. Post\u0119puj zgodnie z instrukcjami, aby je odblokowa\u0107:\\n1. Zresetuj urz\u0105dzenie do ustawie\u0144 fabrycznych.\\n2. Dodaj urz\u0105dzenie w oficjalnej aplikacji do swojej sieci.\\n3. Stop! Nie ko\u0144cz konfiguracji w aplikacji tylko j\u0105 zamknij.\\n4. Potwierd\u017a odblokowanie (przycisk Odblokuj).",
+ "description": "Twoje urz\u0105dzenie jest zablokowane. Post\u0119puj zgodnie z instrukcjami, aby je odblokowa\u0107: \nOpcja 1 (preferowana):\n1. W aplikacji Broadlink wejd\u017a w swoje urz\u0105dzenie\n2. Kliknij \"\u2022 \u2022 \u2022\" \n3. Wy\u0142\u0105cz opcje \"Lock Device\"\n\nOpcja 2:\n1. Zresetuj urz\u0105dzenie do ustawie\u0144 fabrycznych. \n2. Dodaj urz\u0105dzenie w oficjalnej aplikacji do swojej sieci. \n3. Stop! Nie ko\u0144cz konfiguracji w aplikacji tylko j\u0105 zamknij. \n4. Potwierd\u017a odblokowanie (przycisk Odblokuj).",
"title": "Odblokuj urz\u0105dzeniem"
},
"unlock": {
diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json
index f7d0cfab3d1215..617c508b8c2e7c 100644
--- a/homeassistant/components/broadlink/translations/ru.json
+++ b/homeassistant/components/broadlink/translations/ru.json
@@ -2,9 +2,10 @@
"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": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.",
+ "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\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."
},
"error": {
diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json
new file mode 100644
index 00000000000000..38d02e42d90829
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/sv.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "not_supported": "Enheten st\u00f6ds inte"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json
index 741e8beb2f7542..01db5167bb2150 100644
--- a/homeassistant/components/broadlink/translations/zh-Hant.json
+++ b/homeassistant/components/broadlink/translations/zh-Hant.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u6b64\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740",
+ "not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py
index bd124d1e1acfb6..6d3bbc5b6c1f01 100644
--- a/homeassistant/components/broadlink/updater.py
+++ b/homeassistant/components/broadlink/updater.py
@@ -1,12 +1,15 @@
"""Support for fetching data from Broadlink devices."""
from abc import ABC, abstractmethod
from datetime import timedelta
+from functools import partial
import logging
+import broadlink as blk
from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
CommandNotSupportedError,
+ NetworkTimeoutError,
StorageError,
)
@@ -18,6 +21,9 @@
def get_update_manager(device):
"""Return an update manager for a given Broadlink device."""
+ if device.api.model.startswith("RM mini"):
+ return BroadlinkRMMini3UpdateManager(device)
+
update_managers = {
"A1": BroadlinkA1UpdateManager,
"MP1": BroadlinkMP1UpdateManager,
@@ -42,7 +48,7 @@ def __init__(self, device):
self.coordinator = DataUpdateCoordinator(
device.hass,
_LOGGER,
- name="device",
+ name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
update_method=self.async_update,
update_interval=timedelta(minutes=1),
)
@@ -61,14 +67,20 @@ async def async_update(self):
):
self.available = False
_LOGGER.warning(
- "Disconnected from the device at %s", self.device.api.host[0]
+ "Disconnected from %s (%s at %s)",
+ self.device.name,
+ self.device.api.model,
+ self.device.api.host[0],
)
raise UpdateFailed(err) from err
else:
if self.available is False:
_LOGGER.warning(
- "Connected to the device at %s", self.device.api.host[0]
+ "Connected to %s (%s at %s)",
+ self.device.name,
+ self.device.api.model,
+ self.device.api.host[0],
)
self.available = True
self.last_update = dt.utcnow()
@@ -95,6 +107,22 @@ async def async_fetch_data(self):
return await self.device.async_request(self.device.api.check_power)
+class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager):
+ """Manages updates for Broadlink RM mini 3 devices."""
+
+ async def async_fetch_data(self):
+ """Fetch data from the device."""
+ hello = partial(
+ blk.discover,
+ discover_ip_address=self.device.api.host[0],
+ timeout=self.device.api.timeout,
+ )
+ devices = await self.device.hass.async_add_executor_job(hello)
+ if not devices:
+ raise NetworkTimeoutError("The device is offline")
+ return {}
+
+
class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
"""Manages updates for Broadlink RM2 and RM4 devices."""
diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py
index 8b3a9539cc35ea..306945b0424026 100644
--- a/homeassistant/components/brother/config_flow.py
+++ b/homeassistant/components/brother/config_flow.py
@@ -59,7 +59,7 @@ async def async_step_user(self, user_input=None):
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except ConnectionError:
- errors["base"] = "connection_error"
+ errors["base"] = "cannot_connect"
except SnmpError:
errors["base"] = "snmp_error"
except UnsupportedModel:
@@ -72,7 +72,7 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
if discovery_info is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
if not discovery_info.get("name") or not discovery_info["name"].startswith(
"Brother"
@@ -86,7 +86,7 @@ async def async_step_zeroconf(self, discovery_info):
try:
await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModel):
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
# Check if already configured
await self.async_set_unique_id(self.brother.serial.lower())
diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json
index 264992a7eae3e4..358f76e77b88bf 100644
--- a/homeassistant/components/brother/strings.json
+++ b/homeassistant/components/brother/strings.json
@@ -19,12 +19,12 @@
},
"error": {
"wrong_host": "Invalid hostname or IP address.",
- "connection_error": "Connection error.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"snmp_error": "SNMP server turned off or printer not supported."
},
"abort": {
"unsupported_model": "This printer model is not supported.",
- "already_configured": "This printer is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/brother/translations/ca.json b/homeassistant/components/brother/translations/ca.json
index 6f8c1ca912f25d..e32d26ff79aaca 100644
--- a/homeassistant/components/brother/translations/ca.json
+++ b/homeassistant/components/brother/translations/ca.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Aquesta impressora ja est\u00e0 configurada.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"unsupported_model": "Aquest model d'impressora no \u00e9s compatible."
},
"error": {
- "connection_error": "Error de connexi\u00f3.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "connection_error": "Ha fallat la connexi\u00f3",
"snmp_error": "El servidor SNMP s'ha tancat o la impressora no \u00e9s compatible.",
"wrong_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids."
},
diff --git a/homeassistant/components/brother/translations/el.json b/homeassistant/components/brother/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/brother/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/translations/en.json b/homeassistant/components/brother/translations/en.json
index 8cd1ed46919578..af5cc681d51ce2 100644
--- a/homeassistant/components/brother/translations/en.json
+++ b/homeassistant/components/brother/translations/en.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "This printer is already configured.",
+ "already_configured": "Device is already configured",
"unsupported_model": "This printer model is not supported."
},
"error": {
- "connection_error": "Connection error.",
+ "cannot_connect": "Failed to connect",
+ "connection_error": "Failed to connect",
"snmp_error": "SNMP server turned off or printer not supported.",
"wrong_host": "Invalid hostname or IP address."
},
diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json
index 51e1492be13cbb..565b71fe7d2c0e 100644
--- a/homeassistant/components/brother/translations/es.json
+++ b/homeassistant/components/brother/translations/es.json
@@ -5,6 +5,7 @@
"unsupported_model": "Este modelo de impresora no es compatible."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "Error de conexi\u00f3n.",
"snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.",
"wrong_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos."
diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json
new file mode 100644
index 00000000000000..1aac71fc60d899
--- /dev/null
+++ b/homeassistant/components/brother/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "unsupported_model": "Seda printeri mudelit ei toetata."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "\u00dchenduse t\u00f5rge.",
+ "snmp_error": "SNMP-server on v\u00e4lja l\u00fclitatud v\u00f5i printerit ei toetata.",
+ "wrong_host": "Sobimatu hostinimi v\u00f5i IP-aadress."
+ },
+ "flow_title": "Brotheri printer: {model} {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "type": "Printeri t\u00fc\u00fcp"
+ },
+ "description": "Seadistage Brotheri printeri sidumine. Kui teil on seadistamisega probleeme minge aadressile https://www.home-assistant.io/integrations/brother"
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "type": "Printeri t\u00fc\u00fcp"
+ },
+ "description": "Kas soovite lisada Home Assistanti Brotheri printeri {model} seerianumbriga \" {serial_number} \"?",
+ "title": "Avastatud Brotheri printer"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json
index e47b046e59e5da..83b360b1706738 100644
--- a/homeassistant/components/brother/translations/fr.json
+++ b/homeassistant/components/brother/translations/fr.json
@@ -5,6 +5,7 @@
"unsupported_model": "Ce mod\u00e8le d'imprimante n'est pas pris en charge."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Erreur 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."
diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json
index 47d378606f1a1b..47191618bc8cad 100644
--- a/homeassistant/components/brother/translations/it.json
+++ b/homeassistant/components/brother/translations/it.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Questa stampante \u00e8 gi\u00e0 configurata.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"unsupported_model": "Questo modello di stampante non \u00e8 supportato."
},
"error": {
- "connection_error": "Errore di connessione.",
+ "cannot_connect": "Impossibile connettersi",
+ "connection_error": "Impossibile connettersi",
"snmp_error": "Server SNMP spento o stampante non supportata.",
"wrong_host": "Nome host o indirizzo IP non valido."
},
diff --git a/homeassistant/components/brother/translations/no.json b/homeassistant/components/brother/translations/no.json
index bfc5d811f42ada..6fafe9b1d3a0a2 100644
--- a/homeassistant/components/brother/translations/no.json
+++ b/homeassistant/components/brother/translations/no.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Denne skriveren er allerede konfigurert.",
+ "already_configured": "Enheten er allerede konfigurert",
"unsupported_model": "Denne skrivermodellen er ikke st\u00f8ttet."
},
"error": {
- "connection_error": "Tilkoblingen mislyktes.",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "connection_error": "Tilkobling mislyktes.",
"snmp_error": "SNMP verten er skrudd av eller printeren er ikke st\u00f8ttet.",
"wrong_host": "Ugyldig vertsnavn eller IP-adresse."
},
diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json
index cb7c3365628105..250e7d81f31446 100644
--- a/homeassistant/components/brother/translations/pl.json
+++ b/homeassistant/components/brother/translations/pl.json
@@ -5,6 +5,7 @@
"unsupported_model": "Ten model drukarki nie jest obs\u0142ugiwany."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "B\u0142\u0105d po\u0142\u0105czenia.",
"snmp_error": "Serwer SNMP wy\u0142\u0105czony lub drukarka nie jest obs\u0142ugiwana.",
"wrong_host": "Niepoprawna nazwa hosta lub adres IP drukarki."
diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json
index f880f754590dcf..635fbf87f48a59 100644
--- a/homeassistant/components/brother/translations/ru.json
+++ b/homeassistant/components/brother/translations/ru.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
"unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f."
},
"error": {
- "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\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.",
"snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.",
"wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441."
},
diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json
index a1d35a44984418..36455eaf085096 100644
--- a/homeassistant/components/brother/translations/zh-Hant.json
+++ b/homeassistant/components/brother/translations/zh-Hant.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002"
},
"error": {
- "connection_error": "\u9023\u7dda\u932f\u8aa4\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "connection_error": "\u9023\u7dda\u5931\u6557",
"snmp_error": "SNMP \u4f3a\u670d\u5668\u70ba\u95dc\u9589\u72c0\u614b\u6216\u5370\u8868\u6a5f\u4e0d\u652f\u63f4\u3002",
"wrong_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740"
},
diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py
index 7f36874c40ef2e..ceb56ba03fa750 100644
--- a/homeassistant/components/brunt/cover.py
+++ b/homeassistant/components/brunt/cover.py
@@ -7,6 +7,7 @@
from homeassistant.components.cover import (
ATTR_POSITION,
+ DEVICE_CLASS_WINDOW,
PLATFORM_SCHEMA,
SUPPORT_CLOSE,
SUPPORT_OPEN,
@@ -19,7 +20,6 @@
_LOGGER = logging.getLogger(__name__)
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
-DEVICE_CLASS = "window"
ATTR_REQUEST_POSITION = "request_position"
NOTIFICATION_ID = "brunt_notification"
@@ -141,7 +141,7 @@ def device_state_attributes(self):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASS
+ return DEVICE_CLASS_WINDOW
@property
def supported_features(self):
diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py
index 1ecfc239af6fd4..e0e806ae205a54 100644
--- a/homeassistant/components/bsblan/config_flow.py
+++ b/homeassistant/components/bsblan/config_flow.py
@@ -39,7 +39,7 @@ async def async_step_user(
passkey=user_input.get(CONF_PASSKEY),
)
except BSBLanError:
- return self._show_setup_form({"base": "connection_error"})
+ return self._show_setup_form({"base": "cannot_connect"})
# Check if already configured
await self.async_set_unique_id(info.device_identification)
diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json
index 2e7c63f4d3a2a5..1003f75a4a3339 100644
--- a/homeassistant/components/bsblan/strings.json
+++ b/homeassistant/components/bsblan/strings.json
@@ -14,10 +14,10 @@
}
},
"error": {
- "connection_error": "Failed to connect to BSB-Lan device."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/bsblan/translations/ca.json b/homeassistant/components/bsblan/translations/ca.json
index 2fcab5683de5b9..6096784c879908 100644
--- a/homeassistant/components/bsblan/translations/ca.json
+++ b/homeassistant/components/bsblan/translations/ca.json
@@ -4,6 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu BSB-Lan."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/el.json b/homeassistant/components/bsblan/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/en.json b/homeassistant/components/bsblan/translations/en.json
index ce745b351b13a4..b8e3d0b5495116 100644
--- a/homeassistant/components/bsblan/translations/en.json
+++ b/homeassistant/components/bsblan/translations/en.json
@@ -4,6 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to BSB-Lan device."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json
index e22dbfa75ec516..319948ff5d19cf 100644
--- a/homeassistant/components/bsblan/translations/es.json
+++ b/homeassistant/components/bsblan/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se ha podido conectar con el dispositivo BSB-Lan."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/et.json b/homeassistant/components/bsblan/translations/et.json
new file mode 100644
index 00000000000000..81308dd3b50651
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "BSB-Lan seadmega \u00fchenduse loomine nurjus."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json
index 48b235aa1770a5..1b912eaa93b476 100644
--- a/homeassistant/components/bsblan/translations/fr.json
+++ b/homeassistant/components/bsblan/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter \u00e0 l'appareil BSB-Lan."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/it.json b/homeassistant/components/bsblan/translations/it.json
index 421a5ad975a34f..2ff7f173d17962 100644
--- a/homeassistant/components/bsblan/translations/it.json
+++ b/homeassistant/components/bsblan/translations/it.json
@@ -4,6 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo BSB-Lan."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json
index c1909b19508901..c92ccbebb62b73 100644
--- a/homeassistant/components/bsblan/translations/nl.json
+++ b/homeassistant/components/bsblan/translations/nl.json
@@ -3,6 +3,7 @@
"step": {
"user": {
"data": {
+ "host": "Host",
"port": "Poort"
}
}
diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json
index 040349997f444e..b631b28b10edda 100644
--- a/homeassistant/components/bsblan/translations/no.json
+++ b/homeassistant/components/bsblan/translations/no.json
@@ -4,6 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til BSB-Lan-enheten."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json
index 21b4208576162a..6288eab9604e62 100644
--- a/homeassistant/components/bsblan/translations/pl.json
+++ b/homeassistant/components/bsblan/translations/pl.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem BSB_LAN."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json
index 60d0ad4b6536e9..3d4009abb7bb31 100644
--- a/homeassistant/components/bsblan/translations/ru.json
+++ b/homeassistant/components/bsblan/translations/ru.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"flow_title": "BSB-Lan: {name}",
diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json
index 09f15ff84f89e0..81de5bf53658de 100644
--- a/homeassistant/components/bsblan/translations/zh-Hant.json
+++ b/homeassistant/components/bsblan/translations/zh-Hant.json
@@ -4,6 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "BSB-Lan \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"flow_title": "BSB-Lan\uff1a{name}",
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index ba0063ebb85864..b1e41122dc002d 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -30,7 +30,9 @@
DEGREE,
IRRADIATION_WATTS_PER_SQUARE_METER,
LENGTH_KILOMETERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
+ PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
TIME_HOURS,
@@ -78,25 +80,29 @@
"windforce": ["Wind force", "Bft", "mdi:weather-windy"],
"winddirection": ["Wind direction", None, "mdi:compass-outline"],
"windazimuth": ["Wind direction azimuth", DEGREE, "mdi:compass-outline"],
- "pressure": ["Pressure", "hPa", "mdi:gauge"],
+ "pressure": ["Pressure", PRESSURE_HPA, "mdi:gauge"],
"visibility": ["Visibility", LENGTH_KILOMETERS, None],
"windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
- "precipitation": ["Precipitation", f"mm/{TIME_HOURS}", "mdi:weather-pouring"],
+ "precipitation": [
+ "Precipitation",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ "mdi:weather-pouring",
+ ],
"irradiance": ["Irradiance", IRRADIATION_WATTS_PER_SQUARE_METER, "mdi:sunglasses"],
"precipitation_forecast_average": [
"Precipitation forecast average",
- f"mm/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"mdi:weather-pouring",
],
"precipitation_forecast_total": [
"Precipitation forecast total",
- "mm",
+ LENGTH_MILLIMETERS,
"mdi:weather-pouring",
],
# new in json api (>1.0.0):
- "rainlast24hour": ["Rain last 24h", "mm", "mdi:weather-pouring"],
+ "rainlast24hour": ["Rain last 24h", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
# new in json api (>1.0.0):
- "rainlasthour": ["Rain last hour", "mm", "mdi:weather-pouring"],
+ "rainlasthour": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
"temperature_1d": ["Temperature 1d", TEMP_CELSIUS, "mdi:thermometer"],
"temperature_2d": ["Temperature 2d", TEMP_CELSIUS, "mdi:thermometer"],
"temperature_3d": ["Temperature 3d", TEMP_CELSIUS, "mdi:thermometer"],
@@ -107,23 +113,23 @@
"mintemp_3d": ["Minimum temperature 3d", TEMP_CELSIUS, "mdi:thermometer"],
"mintemp_4d": ["Minimum temperature 4d", TEMP_CELSIUS, "mdi:thermometer"],
"mintemp_5d": ["Minimum temperature 5d", TEMP_CELSIUS, "mdi:thermometer"],
- "rain_1d": ["Rain 1d", "mm", "mdi:weather-pouring"],
- "rain_2d": ["Rain 2d", "mm", "mdi:weather-pouring"],
- "rain_3d": ["Rain 3d", "mm", "mdi:weather-pouring"],
- "rain_4d": ["Rain 4d", "mm", "mdi:weather-pouring"],
- "rain_5d": ["Rain 5d", "mm", "mdi:weather-pouring"],
+ "rain_1d": ["Rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "rain_2d": ["Rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "rain_3d": ["Rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "rain_4d": ["Rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "rain_5d": ["Rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
# new in json api (>1.0.0):
- "minrain_1d": ["Minimum rain 1d", "mm", "mdi:weather-pouring"],
- "minrain_2d": ["Minimum rain 2d", "mm", "mdi:weather-pouring"],
- "minrain_3d": ["Minimum rain 3d", "mm", "mdi:weather-pouring"],
- "minrain_4d": ["Minimum rain 4d", "mm", "mdi:weather-pouring"],
- "minrain_5d": ["Minimum rain 5d", "mm", "mdi:weather-pouring"],
+ "minrain_1d": ["Minimum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "minrain_2d": ["Minimum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "minrain_3d": ["Minimum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "minrain_4d": ["Minimum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "minrain_5d": ["Minimum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
# new in json api (>1.0.0):
- "maxrain_1d": ["Maximum rain 1d", "mm", "mdi:weather-pouring"],
- "maxrain_2d": ["Maximum rain 2d", "mm", "mdi:weather-pouring"],
- "maxrain_3d": ["Maximum rain 3d", "mm", "mdi:weather-pouring"],
- "maxrain_4d": ["Maximum rain 4d", "mm", "mdi:weather-pouring"],
- "maxrain_5d": ["Maximum rain 5d", "mm", "mdi:weather-pouring"],
+ "maxrain_1d": ["Maximum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "maxrain_2d": ["Maximum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "maxrain_3d": ["Maximum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "maxrain_4d": ["Maximum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
+ "maxrain_5d": ["Maximum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"],
"rainchance_1d": ["Rainchance 1d", PERCENTAGE, "mdi:weather-pouring"],
"rainchance_2d": ["Rainchance 2d", PERCENTAGE, "mdi:weather-pouring"],
"rainchance_3d": ["Rainchance 3d", PERCENTAGE, "mdi:weather-pouring"],
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index f6b909231cab09..25505800709284 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -19,6 +19,7 @@
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
+ ATTR_MEDIA_EXTRA,
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
)
@@ -35,6 +36,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_FILENAME,
+ CONTENT_TYPE_MULTIPART,
EVENT_HOMEASSISTANT_START,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -46,7 +48,7 @@
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity import Entity, entity_sources
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.network import get_url
from homeassistant.loader import bind_hass
@@ -184,7 +186,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval):
This method must be run in the event loop.
"""
response = web.StreamResponse()
- response.content_type = "multipart/x-mixed-replace; boundary=--frameboundary"
+ response.content_type = CONTENT_TYPE_MULTIPART.format("--frameboundary")
await response.prepare(request)
async def write_to_mjpeg_stream(img_bytes):
@@ -695,14 +697,47 @@ async def async_handle_play_stream_service(camera, service_call):
options=camera.stream_options,
)
data = {
- ATTR_ENTITY_ID: entity_ids,
ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}",
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt],
}
- await hass.services.async_call(
- DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context
- )
+ # It is required to send a different payload for cast media players
+ cast_entity_ids = [
+ entity
+ for entity, source in entity_sources(hass).items()
+ if entity in entity_ids and source["domain"] == "cast"
+ ]
+ other_entity_ids = list(set(entity_ids) - set(cast_entity_ids))
+
+ if cast_entity_ids:
+ await hass.services.async_call(
+ DOMAIN_MP,
+ SERVICE_PLAY_MEDIA,
+ {
+ ATTR_ENTITY_ID: cast_entity_ids,
+ **data,
+ ATTR_MEDIA_EXTRA: {
+ "stream_type": "LIVE",
+ "media_info": {
+ "hlsVideoSegmentFormat": "fmp4",
+ },
+ },
+ },
+ blocking=True,
+ context=service_call.context,
+ )
+
+ if other_entity_ids:
+ await hass.services.async_call(
+ DOMAIN_MP,
+ SERVICE_PLAY_MEDIA,
+ {
+ ATTR_ENTITY_ID: other_entity_ids,
+ **data,
+ },
+ blocking=True,
+ context=service_call.context,
+ )
async def async_handle_record_service(camera, call):
diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py
index 165787a7f46a1b..64f2d00735eabe 100644
--- a/homeassistant/components/canary/__init__.py
+++ b/homeassistant/components/canary/__init__.py
@@ -1,4 +1,5 @@
"""Support for Canary devices."""
+import asyncio
from datetime import timedelta
import logging
@@ -6,20 +7,26 @@
from requests import ConnectTimeout, HTTPError
import voluptuous as vol
+from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
-from homeassistant.helpers import discovery
+from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_COORDINATOR,
+ DATA_UNDO_UPDATE_LISTENER,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-NOTIFICATION_ID = "canary_notification"
-NOTIFICATION_TITLE = "Canary Setup"
-
-DOMAIN = "canary"
-DATA_CANARY = "canary"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
-DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema(
{
@@ -34,90 +41,109 @@
extra=vol.ALLOW_EXTRA,
)
-CANARY_COMPONENTS = ["alarm_control_panel", "camera", "sensor"]
+PLATFORMS = ["alarm_control_panel", "camera", "sensor"]
-def setup(hass, config):
- """Set up the Canary component."""
- conf = config[DOMAIN]
- username = conf[CONF_USERNAME]
- password = conf[CONF_PASSWORD]
- timeout = conf[CONF_TIMEOUT]
+async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
+ """Set up the Canary integration."""
+ hass.data.setdefault(DOMAIN, {})
- try:
- hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
- except (ConnectTimeout, HTTPError) as ex:
- _LOGGER.error("Unable to connect to Canary service: %s", str(ex))
- hass.components.persistent_notification.create(
- f"Error: {ex}
You will need to restart hass after fixing.",
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
- return False
+ if hass.config_entries.async_entries(DOMAIN):
+ return True
- for component in CANARY_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS
+ if CAMERA_DOMAIN in config:
+ camera_config = next(
+ (item for item in config[CAMERA_DOMAIN] if item["platform"] == DOMAIN),
+ None,
+ )
+ if camera_config:
+ ffmpeg_arguments = camera_config.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ )
+
+ if DOMAIN in config:
+ if ffmpeg_arguments != DEFAULT_FFMPEG_ARGUMENTS:
+ config[DOMAIN][CONF_FFMPEG_ARGUMENTS] = ffmpeg_arguments
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config[DOMAIN],
+ )
+ )
return True
-class CanaryData:
- """Get the latest data and update the states."""
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up Canary from a config entry."""
+ if not entry.options:
+ options = {
+ CONF_FFMPEG_ARGUMENTS: entry.data.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ ),
+ CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ }
+ hass.config_entries.async_update_entry(entry, options=options)
+
+ try:
+ canary_api = await hass.async_add_executor_job(_get_canary_api_instance, entry)
+ except (ConnectTimeout, HTTPError) as error:
+ _LOGGER.error("Unable to connect to Canary service: %s", str(error))
+ raise ConfigEntryNotReady from error
+
+ coordinator = CanaryDataUpdateCoordinator(hass, api=canary_api)
+ await coordinator.async_refresh()
- def __init__(self, username, password, timeout):
- """Init the Canary data object."""
+ if not coordinator.last_update_success:
+ raise ConfigEntryNotReady
- self._api = Api(username, password, timeout)
+ undo_listener = entry.add_update_listener(_async_update_listener)
- self._locations_by_id = {}
- self._readings_by_device_id = {}
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_COORDINATOR: coordinator,
+ DATA_UNDO_UPDATE_LISTENER: undo_listener,
+ }
- self.update()
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self, **kwargs):
- """Get the latest data from py-canary."""
- for location in self._api.get_locations():
- location_id = location.location_id
+ return True
- self._locations_by_id[location_id] = location
- for device in location.devices:
- if device.is_online:
- self._readings_by_device_id[
- device.device_id
- ] = self._api.get_latest_readings(device.device_id)
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
- @property
- def locations(self):
- """Return a list of locations."""
- return self._locations_by_id.values()
+ if unload_ok:
+ hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
+ hass.data[DOMAIN].pop(entry.entry_id)
- def get_location(self, location_id):
- """Return a location based on location_id."""
- return self._locations_by_id.get(location_id, [])
+ return unload_ok
- def get_readings(self, device_id):
- """Return a list of readings based on device_id."""
- return self._readings_by_device_id.get(device_id, [])
- def get_reading(self, device_id, sensor_type):
- """Return reading for device_id and sensor type."""
- readings = self._readings_by_device_id.get(device_id, [])
- return next(
- (
- reading.value
- for reading in readings
- if reading.sensor_type == sensor_type
- ),
- None,
- )
+async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None:
+ """Handle options update."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
- def set_location_mode(self, location_id, mode_name, is_private=False):
- """Set location mode."""
- self._api.set_location_mode(location_id, mode_name, is_private)
- self.update(no_throttle=True)
+def _get_canary_api_instance(entry: ConfigEntry) -> Api:
+ """Initialize a new instance of CanaryApi."""
+ canary = Api(
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
+ entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ )
- def get_live_stream_session(self, device):
- """Return live stream session."""
- return self._api.get_live_stream_session(device)
+ return canary
diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py
index ea0e3078b0c269..957b659eb79c89 100644
--- a/homeassistant/components/canary/alarm_control_panel.py
+++ b/homeassistant/components/canary/alarm_control_panel.py
@@ -1,5 +1,6 @@
"""Support for Canary alarm."""
import logging
+from typing import Callable, List
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
@@ -9,55 +10,78 @@
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
)
+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,
)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import DATA_CANARY
+from .const import DATA_COORDINATOR, DOMAIN
+from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Canary alarms."""
- data = hass.data[DATA_CANARY]
- devices = [CanaryAlarm(data, location.location_id) for location in data.locations]
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], bool], None],
+) -> None:
+ """Set up Canary alarm control panels based on a config entry."""
+ coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
+ DATA_COORDINATOR
+ ]
+ alarms = [
+ CanaryAlarm(coordinator, location)
+ for location_id, location in coordinator.data["locations"].items()
+ ]
- add_entities(devices, True)
+ async_add_entities(alarms, True)
-class CanaryAlarm(AlarmControlPanelEntity):
+class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Canary alarm control panel."""
- def __init__(self, data, location_id):
+ def __init__(self, coordinator, location):
"""Initialize a Canary security camera."""
- self._data = data
- self._location_id = location_id
+ super().__init__(coordinator)
+ self._location_id = location.location_id
+ self._location_name = location.name
+
+ @property
+ def location(self):
+ """Return information about the location."""
+ return self.coordinator.data["locations"][self._location_id]
@property
def name(self):
"""Return the name of the alarm."""
- location = self._data.get_location(self._location_id)
- return location.name
+ return self._location_name
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the alarm."""
+ return str(self._location_id)
@property
def state(self):
"""Return the state of the device."""
- location = self._data.get_location(self._location_id)
-
- if location.is_private:
+ if self.location.is_private:
return STATE_ALARM_DISARMED
- mode = location.mode
+ mode = self.location.mode
if mode.name == LOCATION_MODE_AWAY:
return STATE_ALARM_ARMED_AWAY
if mode.name == LOCATION_MODE_HOME:
return STATE_ALARM_ARMED_HOME
if mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT
+
return None
@property
@@ -68,26 +92,24 @@ def supported_features(self) -> int:
@property
def device_state_attributes(self):
"""Return the state attributes."""
- location = self._data.get_location(self._location_id)
- return {"private": location.is_private}
+ return {"private": self.location.is_private}
def alarm_disarm(self, code=None):
"""Send disarm command."""
- location = self._data.get_location(self._location_id)
- self._data.set_location_mode(self._location_id, location.mode.name, True)
+ self.coordinator.canary.set_location_mode(
+ self._location_id, self.location.mode.name, True
+ )
def alarm_arm_home(self, code=None):
"""Send arm home command."""
- self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
+ self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
- self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
+ self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
- self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
-
- def update(self):
- """Get the latest state of the sensor."""
- self._data.update()
+ self.coordinator.canary.set_location_mode(
+ self._location_id, LOCATION_MODE_NIGHT
+ )
diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py
index 3ba7f094da145c..4d0a4a0d169a4d 100644
--- a/homeassistant/components/canary/camera.py
+++ b/homeassistant/components/canary/camera.py
@@ -2,6 +2,7 @@
import asyncio
from datetime import timedelta
import logging
+from typing import Callable, List
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
@@ -9,78 +10,121 @@
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
+from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import Throttle
-from . import DATA_CANARY, DEFAULT_TIMEOUT
+from .const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_COORDINATOR,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+ MANUFACTURER,
+)
+from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
-DEFAULT_ARGUMENTS = "-pred 1"
-
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string}
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(
+ CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS
+ ): cv.string
+ }
+ ),
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Canary sensors."""
- if discovery_info is not None:
- return
-
- data = hass.data[DATA_CANARY]
- devices = []
-
- for location in data.locations:
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], bool], None],
+) -> None:
+ """Set up Canary sensors based on a config entry."""
+ coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
+ DATA_COORDINATOR
+ ]
+ ffmpeg_arguments = entry.options.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ )
+ cameras = []
+
+ for location_id, location in coordinator.data["locations"].items():
for device in location.devices:
if device.is_online:
- devices.append(
+ cameras.append(
CanaryCamera(
hass,
- data,
- location,
+ coordinator,
+ location_id,
device,
DEFAULT_TIMEOUT,
- config[CONF_FFMPEG_ARGUMENTS],
+ ffmpeg_arguments,
)
)
- add_entities(devices, True)
+ async_add_entities(cameras, True)
-class CanaryCamera(Camera):
+class CanaryCamera(CoordinatorEntity, Camera):
"""An implementation of a Canary security camera."""
- def __init__(self, hass, data, location, device, timeout, ffmpeg_args):
+ def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args):
"""Initialize a Canary security camera."""
- super().__init__()
-
+ super().__init__(coordinator)
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = ffmpeg_args
- self._data = data
- self._location = location
+ self._location_id = location_id
self._device = device
+ self._device_id = device.device_id
+ self._device_name = device.name
+ self._device_type_name = device.device_type["name"]
self._timeout = timeout
self._live_stream_session = None
+ @property
+ def location(self):
+ """Return information about the location."""
+ return self.coordinator.data["locations"][self._location_id]
+
@property
def name(self):
"""Return the name of this device."""
- return self._device.name
+ return self._device_name
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this camera."""
+ return str(self._device_id)
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, str(self._device_id))},
+ "name": self._device_name,
+ "model": self._device_type_name,
+ "manufacturer": MANUFACTURER,
+ }
@property
def is_recording(self):
"""Return true if the device is recording."""
- return self._location.is_recording
+ return self.location.is_recording
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
- return not self._location.is_recording
+ return not self.location.is_recording
async def async_camera_image(self):
"""Return a still image response from the camera."""
@@ -120,4 +164,6 @@ async def handle_async_mjpeg_stream(self, request):
@Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
def renew_live_stream_session(self):
"""Renew live stream session."""
- self._live_stream_session = self._data.get_live_stream_session(self._device)
+ self._live_stream_session = self.coordinator.canary.get_live_stream_session(
+ self._device
+ )
diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py
new file mode 100644
index 00000000000000..dc2822d836a2ae
--- /dev/null
+++ b/homeassistant/components/canary/config_flow.py
@@ -0,0 +1,121 @@
+"""Config flow for Canary."""
+import logging
+from typing import Any, Dict, Optional
+
+from canary.api import Api
+from requests import ConnectTimeout, HTTPError
+import voluptuous as vol
+
+from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow
+from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.core import callback
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+ """Validate the user input allows us to connect.
+
+ Data has the keys from DATA_SCHEMA with values provided by the user.
+ """
+ # constructor does login call
+ Api(
+ data[CONF_USERNAME],
+ data[CONF_PASSWORD],
+ data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ )
+
+ return True
+
+
+class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Canary."""
+
+ VERSION = 1
+ CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return CanaryOptionsFlowHandler(config_entry)
+
+ async def async_step_import(
+ self, user_input: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Handle a flow initiated by configuration file."""
+ return await self.async_step_user(user_input)
+
+ async def async_step_user(
+ self, user_input: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ errors = {}
+ default_username = ""
+
+ if user_input is not None:
+ if CONF_TIMEOUT not in user_input:
+ user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT
+
+ default_username = user_input[CONF_USERNAME]
+
+ try:
+ await self.hass.async_add_executor_job(
+ validate_input, self.hass, user_input
+ )
+ except (ConnectTimeout, HTTPError):
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+ else:
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME],
+ data=user_input,
+ )
+
+ data_schema = {
+ vol.Required(CONF_USERNAME, default=default_username): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(data_schema),
+ errors=errors or {},
+ )
+
+
+class CanaryOptionsFlowHandler(OptionsFlow):
+ """Handle Canary client options."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input: Optional[ConfigType] = None):
+ """Manage Canary options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ options = {
+ vol.Optional(
+ CONF_FFMPEG_ARGUMENTS,
+ default=self.config_entry.options.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ ),
+ ): str,
+ vol.Optional(
+ CONF_TIMEOUT,
+ default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ ): int,
+ }
+
+ return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py
new file mode 100644
index 00000000000000..8219a485ef972c
--- /dev/null
+++ b/homeassistant/components/canary/const.py
@@ -0,0 +1,16 @@
+"""Constants for the Canary integration."""
+
+DOMAIN = "canary"
+
+MANUFACTURER = "Canary Connect, Inc"
+
+# Configuration
+CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
+
+# Data
+DATA_COORDINATOR = "coordinator"
+DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
+
+# Defaults
+DEFAULT_FFMPEG_ARGUMENTS = "-pred 1"
+DEFAULT_TIMEOUT = 10
diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py
new file mode 100644
index 00000000000000..650bc3d70eab05
--- /dev/null
+++ b/homeassistant/components/canary/coordinator.py
@@ -0,0 +1,59 @@
+"""Provides the Canary DataUpdateCoordinator."""
+from datetime import timedelta
+import logging
+
+from async_timeout import timeout
+from canary.api import Api
+from requests import ConnectTimeout, HTTPError
+
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class CanaryDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching Canary data."""
+
+ def __init__(self, hass: HomeAssistantType, *, api: Api):
+ """Initialize global Canary data updater."""
+ self.canary = api
+ update_interval = timedelta(seconds=30)
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=update_interval,
+ )
+
+ def _update_data(self) -> dict:
+ """Fetch data from Canary via sync functions."""
+ locations_by_id = {}
+ readings_by_device_id = {}
+
+ for location in self.canary.get_locations():
+ location_id = location.location_id
+ locations_by_id[location_id] = location
+
+ for device in location.devices:
+ if device.is_online:
+ readings_by_device_id[
+ device.device_id
+ ] = self.canary.get_latest_readings(device.device_id)
+
+ return {
+ "locations": locations_by_id,
+ "readings": readings_by_device_id,
+ }
+
+ async def _async_update_data(self) -> dict:
+ """Fetch data from Canary."""
+
+ try:
+ async with timeout(15):
+ return await self.hass.async_add_executor_job(self._update_data)
+ except (ConnectTimeout, HTTPError) as error:
+ raise UpdateFailed(f"Invalid response from API: {error}") from error
diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json
index e383cb7514b9fa..b4598d64087f75 100644
--- a/homeassistant/components/canary/manifest.json
+++ b/homeassistant/components/canary/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/canary",
"requirements": ["py-canary==0.5.0"],
"dependencies": ["ffmpeg"],
- "codeowners": []
+ "codeowners": [],
+ "config_flow": true
}
diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py
index 5f1b1fe906b48f..99dcdf48fceddb 100644
--- a/homeassistant/components/canary/sensor.py
+++ b/homeassistant/components/canary/sensor.py
@@ -1,11 +1,24 @@
"""Support for Canary sensors."""
+from typing import Callable, List
+
from canary.api import SensorType
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ DEVICE_CLASS_TEMPERATURE,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ TEMP_CELSIUS,
+)
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.icon import icon_for_battery_level
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import DATA_CANARY
+from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
+from .coordinator import CanaryDataUpdateCoordinator
SENSOR_VALUE_PRECISION = 2
ATTR_AIR_QUALITY = "air_quality"
@@ -18,13 +31,19 @@
CANARY_FLEX = "Canary Flex"
# Sensor types are defined like so:
-# sensor type name, unit_of_measurement, icon
+# sensor type name, unit_of_measurement, icon, device class, products supported
SENSOR_TYPES = [
- ["temperature", TEMP_CELSIUS, "mdi:thermometer", [CANARY_PRO]],
- ["humidity", PERCENTAGE, "mdi:water-percent", [CANARY_PRO]],
- ["air_quality", None, "mdi:weather-windy", [CANARY_PRO]],
- ["wifi", "dBm", "mdi:wifi", [CANARY_FLEX]],
- ["battery", PERCENTAGE, "mdi:battery-50", [CANARY_FLEX]],
+ ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]],
+ ["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]],
+ ["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]],
+ [
+ "wifi",
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ None,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ [CANARY_FLEX],
+ ],
+ ["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]],
]
STATE_AIR_QUALITY_NORMAL = "normal"
@@ -32,37 +51,77 @@
STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal"
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Canary sensors."""
- data = hass.data[DATA_CANARY]
- devices = []
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], bool], None],
+) -> None:
+ """Set up Canary sensors based on a config entry."""
+ coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
+ DATA_COORDINATOR
+ ]
+ sensors = []
- for location in data.locations:
+ for location in coordinator.data["locations"].values():
for device in location.devices:
if device.is_online:
device_type = device.device_type
for sensor_type in SENSOR_TYPES:
- if device_type.get("name") in sensor_type[3]:
- devices.append(
- CanarySensor(data, sensor_type, location, device)
+ if device_type.get("name") in sensor_type[4]:
+ sensors.append(
+ CanarySensor(coordinator, sensor_type, location, device)
)
- add_entities(devices, True)
+ async_add_entities(sensors, True)
-class CanarySensor(Entity):
+class CanarySensor(CoordinatorEntity, Entity):
"""Representation of a Canary sensor."""
- def __init__(self, data, sensor_type, location, device):
+ def __init__(self, coordinator, sensor_type, location, device):
"""Initialize the sensor."""
- self._data = data
+ super().__init__(coordinator)
self._sensor_type = sensor_type
self._device_id = device.device_id
- self._sensor_value = None
+ self._device_name = device.name
+ self._device_type_name = device.device_type["name"]
sensor_type_name = sensor_type[0].replace("_", " ").title()
self._name = f"{location.name} {device.name} {sensor_type_name}"
+ canary_sensor_type = None
+ if self._sensor_type[0] == "air_quality":
+ canary_sensor_type = SensorType.AIR_QUALITY
+ elif self._sensor_type[0] == "temperature":
+ canary_sensor_type = SensorType.TEMPERATURE
+ elif self._sensor_type[0] == "humidity":
+ canary_sensor_type = SensorType.HUMIDITY
+ elif self._sensor_type[0] == "wifi":
+ canary_sensor_type = SensorType.WIFI
+ elif self._sensor_type[0] == "battery":
+ canary_sensor_type = SensorType.BATTERY
+
+ self._canary_type = canary_sensor_type
+
+ @property
+ def reading(self):
+ """Return the device sensor reading."""
+ readings = self.coordinator.data["readings"][self._device_id]
+
+ value = next(
+ (
+ reading.value
+ for reading in readings
+ if reading.sensor_type == self._canary_type
+ ),
+ None,
+ )
+
+ if value is not None:
+ return round(float(value), SENSOR_VALUE_PRECISION)
+
+ return None
+
@property
def name(self):
"""Return the name of the Canary sensor."""
@@ -71,59 +130,52 @@ def name(self):
@property
def state(self):
"""Return the state of the sensor."""
- return self._sensor_value
+ return self.reading
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return f"{self._device_id}_{self._sensor_type[0]}"
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, str(self._device_id))},
+ "name": self._device_name,
+ "model": self._device_type_name,
+ "manufacturer": MANUFACTURER,
+ }
+
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._sensor_type[1]
+ @property
+ def device_class(self):
+ """Device class for the sensor."""
+ return self._sensor_type[3]
+
@property
def icon(self):
"""Icon for the sensor."""
- if self.state is not None and self._sensor_type[0] == "battery":
- return icon_for_battery_level(battery_level=self.state)
-
return self._sensor_type[2]
@property
def device_state_attributes(self):
"""Return the state attributes."""
- if self._sensor_type[0] == "air_quality" and self._sensor_value is not None:
+ reading = self.reading
+
+ if self._sensor_type[0] == "air_quality" and reading is not None:
air_quality = None
- if self._sensor_value <= 0.4:
+ if reading <= 0.4:
air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL
- elif self._sensor_value <= 0.59:
+ elif reading <= 0.59:
air_quality = STATE_AIR_QUALITY_ABNORMAL
- elif self._sensor_value <= 1.0:
+ elif reading <= 1.0:
air_quality = STATE_AIR_QUALITY_NORMAL
return {ATTR_AIR_QUALITY: air_quality}
return None
-
- def update(self):
- """Get the latest state of the sensor."""
- self._data.update()
-
- canary_sensor_type = None
- if self._sensor_type[0] == "air_quality":
- canary_sensor_type = SensorType.AIR_QUALITY
- elif self._sensor_type[0] == "temperature":
- canary_sensor_type = SensorType.TEMPERATURE
- elif self._sensor_type[0] == "humidity":
- canary_sensor_type = SensorType.HUMIDITY
- elif self._sensor_type[0] == "wifi":
- canary_sensor_type = SensorType.WIFI
- elif self._sensor_type[0] == "battery":
- canary_sensor_type = SensorType.BATTERY
-
- value = self._data.get_reading(self._device_id, canary_sensor_type)
-
- if value is not None:
- self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)
diff --git a/homeassistant/components/canary/strings.json b/homeassistant/components/canary/strings.json
new file mode 100644
index 00000000000000..504a5dc2ac1ddf
--- /dev/null
+++ b/homeassistant/components/canary/strings.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "title": "Connect to Canary",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras",
+ "timeout": "Request Timeout (seconds)"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/canary/translations/ca.json b/homeassistant/components/canary/translations/ca.json
new file mode 100644
index 00000000000000..c4b80d7537aa7e
--- /dev/null
+++ b/homeassistant/components/canary/translations/ca.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
+ "unknown": "Error inesperat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "title": "Connexi\u00f3 amb Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Par\u00e0metres enviats a ffmpeg per c\u00e0meres",
+ "timeout": "Temps d'espera de sol\u00b7licitud (segons)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json
new file mode 100644
index 00000000000000..159f961c3a6fa9
--- /dev/null
+++ b/homeassistant/components/canary/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Anfrage-Timeout (Sekunden)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/el.json b/homeassistant/components/canary/translations/el.json
new file mode 100644
index 00000000000000..6b11d64282950e
--- /dev/null
+++ b/homeassistant/components/canary/translations/el.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "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.",
+ "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"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "\u03a4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03b2\u03b9\u03b2\u03ac\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03bf ffmpeg \u03b3\u03b9\u03b1 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2",
+ "timeout": "\u0391\u03af\u03c4\u03b7\u03bc\u03b1 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/en.json b/homeassistant/components/canary/translations/en.json
new file mode 100644
index 00000000000000..1e04d1825f3487
--- /dev/null
+++ b/homeassistant/components/canary/translations/en.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible.",
+ "unknown": "Unexpected error"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ },
+ "title": "Connect to Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras",
+ "timeout": "Request Timeout (seconds)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/es.json b/homeassistant/components/canary/translations/es.json
new file mode 100644
index 00000000000000..1b881d4dcd22a6
--- /dev/null
+++ b/homeassistant/components/canary/translations/es.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n.",
+ "unknown": "Error inesperado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ },
+ "title": "Conectar a Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras",
+ "timeout": "Tiempo de espera de la solicitud (segundos)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/et.json b/homeassistant/components/canary/translations/et.json
new file mode 100644
index 00000000000000..eb0f01ea4d9ae8
--- /dev/null
+++ b/homeassistant/components/canary/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.",
+ "unknown": "Tundmatu viga"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus"
+ },
+ "flow_title": "Canary {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "title": "Loo \u00fchendus Canary-iga"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "P\u00e4ringu ajal\u00f5pp (sekundites)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/fr.json b/homeassistant/components/canary/translations/fr.json
new file mode 100644
index 00000000000000..9bb1761f9fba98
--- /dev/null
+++ b/homeassistant/components/canary/translations/fr.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.",
+ "unknown": "Erreur inattendue"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion"
+ },
+ "flow_title": "Canary : {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "title": "Se connecter \u00e0 Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Arguments transmis \u00e0 ffmpeg pour les cam\u00e9ras",
+ "timeout": "D\u00e9lai d'expiration de la demande (secondes)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/it.json b/homeassistant/components/canary/translations/it.json
new file mode 100644
index 00000000000000..b29a758acaafd5
--- /dev/null
+++ b/homeassistant/components/canary/translations/it.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
+ "unknown": "Errore imprevisto"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "title": "Connettiti a Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Argomenti passati a ffmpeg per le fotocamere",
+ "timeout": "Richiesta Timeout (secondi)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json
new file mode 100644
index 00000000000000..0b1d82bb20a418
--- /dev/null
+++ b/homeassistant/components/canary/translations/ko.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec \ubc1c\uc0dd"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\uc554\ud638",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ },
+ "title": "Canary\uc5d0 \uc5f0\uacb0"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "\uce74\uba54\ub77c ffmpeg\uc5d0 \uc804\ub2ec \ub41c \uc778\uc218",
+ "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/lb.json b/homeassistant/components/canary/translations/lb.json
new file mode 100644
index 00000000000000..0ad059e1fcc2e7
--- /dev/null
+++ b/homeassistant/components/canary/translations/lb.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ },
+ "title": "Mat Canary verbannen"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Argumenter fir ffmpeg fir Kamera",
+ "timeout": "Ufro Z\u00e4itiwwerscheidung (sekonnen)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json
new file mode 100644
index 00000000000000..9681bcd7c371fe
--- /dev/null
+++ b/homeassistant/components/canary/translations/nl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n enkele configuratie mogelijk.",
+ "unknown": "Onverwachte fout"
+ },
+ "error": {
+ "cannot_connect": "Kon niet verbinden"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "title": "Maak verbinding met Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Time-out verzoek (seconden)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/no.json b/homeassistant/components/canary/translations/no.json
new file mode 100644
index 00000000000000..1de0a59b206d7a
--- /dev/null
+++ b/homeassistant/components/canary/translations/no.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
+ "unknown": "Uventet feil"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes."
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "title": "Koble til Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Argumenter sendt til ffmpeg for kameraer",
+ "timeout": "Be om tidsavbrudd (sekunder)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/pl.json b/homeassistant/components/canary/translations/pl.json
new file mode 100644
index 00000000000000..f9c346da393d58
--- /dev/null
+++ b/homeassistant/components/canary/translations/pl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "title": "Po\u0142\u0105czenie z Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Limit czasu \u017c\u0105dania (w sekundach)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/ru.json b/homeassistant/components/canary/translations/ru.json
new file mode 100644
index 00000000000000..146863cf768ba2
--- /dev/null
+++ b/homeassistant/components/canary/translations/ru.json
@@ -0,0 +1,31 @@
+{
+ "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.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u041b\u043e\u0433\u0438\u043d"
+ },
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json
new file mode 100644
index 00000000000000..07463bc8a154fc
--- /dev/null
+++ b/homeassistant/components/canary/translations/zh-Hant.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "flow_title": "Canary\uff1a{name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "title": "\u9023\u7dda\u81f3 Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "\u50b3\u905e\u81f3 ffmpeg \u4e4b\u651d\u5f71\u6a5f\u53c3\u6578",
+ "timeout": "\u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index 49d26431f5bbee..03412d3b6df4ea 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==7.2.1"],
+ "requirements": ["pychromecast==7.5.0"],
"after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]
diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py
index 177babdb476fa2..314236d9f56541 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -21,6 +21,7 @@
from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
+ ATTR_MEDIA_EXTRA,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
@@ -85,7 +86,7 @@
ENTITY_SCHEMA = vol.All(
- cv.deprecated(CONF_HOST, invalidation_version="0.116"),
+ cv.deprecated(CONF_HOST),
vol.Schema(
{
vol.Exclusive(CONF_HOST, "device_identifier"): cv.string,
@@ -96,7 +97,7 @@
)
PLATFORM_SCHEMA = vol.All(
- cv.deprecated(CONF_HOST, invalidation_version="0.116"),
+ cv.deprecated(CONF_HOST),
PLATFORM_SCHEMA.extend(
{
vol.Exclusive(CONF_HOST, "device_identifier"): cv.string,
@@ -171,7 +172,7 @@ async def _async_setup_platform(
# Import CEC IGNORE attributes
pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, [])
hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set())
- hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, dict())
+ hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, {})
info = None
if discovery_info is not None:
@@ -375,9 +376,9 @@ def new_media_status(self, media_status):
if tts_base_url and media_status.content_id.startswith(tts_base_url):
url_description = f" from tts.base_url ({tts_base_url})"
if external_url and media_status.content_id.startswith(external_url):
- url_description = " from external_url ({external_url})"
+ url_description = f" from external_url ({external_url})"
if internal_url and media_status.content_id.startswith(internal_url):
- url_description = " from internal_url ({internal_url})"
+ url_description = f" from internal_url ({internal_url})"
_LOGGER.error(
"Failed to cast media %s%s. Please make sure the URL is: "
@@ -574,7 +575,9 @@ def play_media(self, media_type, media_id, **kwargs):
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
else:
- self._chromecast.media_controller.play_media(media_id, media_type)
+ self._chromecast.media_controller.play_media(
+ media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
+ )
# ========== Properties ==========
@property
diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json
index aed62243f31e30..ad8f0f41ae7b29 100644
--- a/homeassistant/components/cast/strings.json
+++ b/homeassistant/components/cast/strings.json
@@ -2,12 +2,12 @@
"config": {
"step": {
"confirm": {
- "description": "Do you want to set up Google Cast?"
+ "description": "[%key:common::config_flow::description::confirm_setup%]"
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Google Cast is necessary.",
- "no_devices_found": "No Google Cast devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json
index 02a2459fdc0b5e..dc21c371e60fa0 100644
--- a/homeassistant/components/cast/translations/ca.json
+++ b/homeassistant/components/cast/translations/ca.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Google Cast."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
- "description": "Vols configurar Google Cast?"
+ "description": "Vols comen\u00e7ar la configuraci\u00f3?"
}
}
}
diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json
index e06a94f700b661..f05becffed3131 100644
--- a/homeassistant/components/cast/translations/en.json
+++ b/homeassistant/components/cast/translations/en.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "No Google Cast devices found on the network.",
- "single_instance_allowed": "Only a single configuration of Google Cast is necessary."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
- "description": "Do you want to set up Google Cast?"
+ "description": "Do you want to start set up?"
}
}
}
diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json
new file mode 100644
index 00000000000000..05287b5a52b9a2
--- /dev/null
+++ b/homeassistant/components/cast/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi Google Casti seadet.",
+ "single_instance_allowed": "Vajalik on ainult \u00fcks Google Casti konfiguratsioon."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kas soovid seadistada Google Casti?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json
index 6e56218c99210e..0278fe07bfeff9 100644
--- a/homeassistant/components/cast/translations/it.json
+++ b/homeassistant/components/cast/translations/it.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo Google Cast trovato in rete.",
- "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
- "description": "Vuoi configurare Google Cast?"
+ "description": "Vuoi iniziare la configurazione?"
}
}
}
diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json
index b96be399cc8580..b3d6b5d782e97a 100644
--- a/homeassistant/components/cast/translations/no.json
+++ b/homeassistant/components/cast/translations/no.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Kun en konfigurasjon av Google Cast er n\u00f8dvendig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
- "description": "\u00d8nsker du \u00e5 sette opp Google Cast?"
+ "description": "Vil du starte oppsettet?"
}
}
}
diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json
index a62f33832e0b73..85a42bf1be546c 100644
--- a/homeassistant/components/cast/translations/ru.json
+++ b/homeassistant/components/cast/translations/ru.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
- "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?"
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?"
}
}
}
diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json
index 3bb3e70d68865c..91a0dc60be7fc4 100644
--- a/homeassistant/components/cast/translations/zh-Hant.json
+++ b/homeassistant/components/cast/translations/zh-Hant.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f"
+ "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f"
}
}
}
diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json
index fb14e90d586863..bb50f1ee88b447 100644
--- a/homeassistant/components/cert_expiry/strings.json
+++ b/homeassistant/components/cert_expiry/strings.json
@@ -17,8 +17,8 @@
"connection_refused": "Connection refused when connecting to host"
},
"abort": {
- "already_configured": "This host and port combination is already configured",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/cert_expiry/translations/ca.json b/homeassistant/components/cert_expiry/translations/ca.json
index 50d761f03c23f3..42da690550b986 100644
--- a/homeassistant/components/cert_expiry/translations/ca.json
+++ b/homeassistant/components/cert_expiry/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada",
+ "already_configured": "El servei ja est\u00e0 configurat",
"import_failed": "La importaci\u00f3 des de configuraci\u00f3 ha fallat"
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/en.json b/homeassistant/components/cert_expiry/translations/en.json
index 8d551ec2e03922..ff9eb2582f187a 100644
--- a/homeassistant/components/cert_expiry/translations/en.json
+++ b/homeassistant/components/cert_expiry/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "This host and port combination is already configured",
+ "already_configured": "Service is already configured",
"import_failed": "Import from config failed"
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/it.json b/homeassistant/components/cert_expiry/translations/it.json
index 29b26710b626db..232ee01022154e 100644
--- a/homeassistant/components/cert_expiry/translations/it.json
+++ b/homeassistant/components/cert_expiry/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata",
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
"import_failed": "Importazione dalla configurazione non riuscita"
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/no.json b/homeassistant/components/cert_expiry/translations/no.json
index a7aa3d1ab133b3..50a4125a0ff572 100644
--- a/homeassistant/components/cert_expiry/translations/no.json
+++ b/homeassistant/components/cert_expiry/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Denne verts- og portkombinasjonen er allerede konfigurert",
+ "already_configured": "Tjenesten er allerede konfigurert",
"import_failed": "Import fra config mislyktes"
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/ru.json b/homeassistant/components/cert_expiry/translations/ru.json
index a924398f90e73e..6aec708243303c 100644
--- a/homeassistant/components/cert_expiry/translations/ru.json
+++ b/homeassistant/components/cert_expiry/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
+ "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.",
"import_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438."
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/zh-Hant.json b/homeassistant/components/cert_expiry/translations/zh-Hant.json
index 6cfe05f6d1a252..f21603709ac9f4 100644
--- a/homeassistant/components/cert_expiry/translations/zh-Hant.json
+++ b/homeassistant/components/cert_expiry/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"import_failed": "\u532f\u5165\u8a2d\u5b9a\u5931\u6557"
},
"error": {
diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py
index 966dbdee6e2938..09096f44b7403e 100644
--- a/homeassistant/components/clickatell/notify.py
+++ b/homeassistant/components/clickatell/notify.py
@@ -5,7 +5,7 @@
import voluptuous as vol
from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
-from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_OK
+from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_ACCEPTED, HTTP_OK
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -37,5 +37,5 @@ def send_message(self, message="", **kwargs):
data = {"apiKey": self.api_key, "to": self.recipient, "content": message}
resp = requests.get(BASE_API_URL, params=data, timeout=5)
- if (resp.status_code != HTTP_OK) or (resp.status_code != 202):
+ if (resp.status_code != HTTP_OK) or (resp.status_code != HTTP_ACCEPTED):
_LOGGER.error("Error %s : %s", resp.status_code, resp.text)
diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py
index 6f7725ac83577a..3f2b8dc23f2c89 100644
--- a/homeassistant/components/climate/device_action.py
+++ b/homeassistant/components/climate/device_action.py
@@ -5,6 +5,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -61,7 +62,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
CONF_TYPE: "set_hvac_mode",
}
)
- if state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE:
+ if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE:
actions.append(
{
CONF_DEVICE_ID: device_id,
diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py
index 8a5b9ceede8f9d..423efdf8196c4d 100644
--- a/homeassistant/components/climate/device_condition.py
+++ b/homeassistant/components/climate/device_condition.py
@@ -5,6 +5,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -63,7 +64,10 @@ async def async_get_conditions(
}
)
- if state and state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE:
+ if (
+ state
+ and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE
+ ):
conditions.append(
{
CONF_CONDITION: "device",
diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py
new file mode 100644
index 00000000000000..87674da414bee3
--- /dev/null
+++ b/homeassistant/components/climate/group.py
@@ -0,0 +1,20 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import HVAC_MODE_OFF, HVAC_MODES
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states(
+ set(HVAC_MODES) - {HVAC_MODE_OFF},
+ STATE_OFF,
+ )
diff --git a/homeassistant/components/climate/translations/et.json b/homeassistant/components/climate/translations/et.json
index 1c4a6a5ff11218..7be57f4cdaa013 100644
--- a/homeassistant/components/climate/translations/et.json
+++ b/homeassistant/components/climate/translations/et.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "Kliimaseadme {entity_name} re\u017eiimi muutmine",
+ "set_preset_mode": "Olemi {entity_name} eelseadistuse muutmine"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} on seatud kindlale kliimaseadme re\u017eiimile",
+ "is_preset_mode": "{entity_name} on seatud kindlale eelseadistatud re\u017eiimile"
+ },
+ "trigger_type": {
+ "current_humidity_changed": "{entity_name} m\u00f5\u00f5detud niiskus muutus",
+ "current_temperature_changed": "{entity_name} m\u00f5\u00f5detud temperatuur muutus",
+ "hvac_mode_changed": "{entity_name} kliimasedame re\u017eiim on muudetud"
+ }
+ },
"state": {
"_": {
"auto": "Automaatne",
@@ -10,5 +25,5 @@
"off": "V\u00e4ljas"
}
},
- "title": "Kliima"
+ "title": "Kliimaseade"
}
\ No newline at end of file
diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json
index 227e0e1f4ef98a..8d636c386e5479 100644
--- a/homeassistant/components/climate/translations/uk.json
+++ b/homeassistant/components/climate/translations/uk.json
@@ -1,12 +1,27 @@
{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c HVAC \u043d\u0430 {entity_name}",
+ "set_preset_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430 {entity_name}"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c HVAC",
+ "is_preset_mode": "{entity_name} \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e \u043d\u0430 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c"
+ },
+ "trigger_type": {
+ "current_humidity_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0430 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c \u0437\u043c\u0456\u043d\u0435\u043d\u0430",
+ "current_temperature_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443 \u0437\u043c\u0456\u043d\u0435\u043d\u043e",
+ "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c HVAC \u0437\u043c\u0456\u043d\u0435\u043d\u043e"
+ }
+ },
"state": {
"_": {
"auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439",
"cool": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
"dry": "\u041e\u0441\u0443\u0448\u0435\u043d\u043d\u044f",
"fan_only": "\u041b\u0438\u0448\u0435 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440",
- "heat": "\u041e\u0431\u0456\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f",
- "heat_cool": "\u041e\u043f\u0430\u043b\u0435\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "heat": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f",
+ "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e"
}
},
diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py
index baa63679d42c1b..0e3b20fa011592 100644
--- a/homeassistant/components/cloud/binary_sensor.py
+++ b/homeassistant/components/cloud/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for Home Assistant Cloud binary sensors."""
import asyncio
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorEntity,
+)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
@@ -44,7 +47,7 @@ def is_on(self) -> bool:
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""
- return "connectivity"
+ return DEVICE_CLASS_CONNECTIVITY
@property
def available(self) -> bool:
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 00a2ddb46630ae..3075f6a3f9d93c 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -19,7 +19,13 @@
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.websocket_api import const as ws_const
-from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_OK
+from homeassistant.const import (
+ HTTP_BAD_GATEWAY,
+ HTTP_BAD_REQUEST,
+ HTTP_INTERNAL_SERVER_ERROR,
+ HTTP_OK,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.core import callback
from .const import (
@@ -73,7 +79,10 @@
HTTP_INTERNAL_SERVER_ERROR,
"Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.",
),
- asyncio.TimeoutError: (502, "Unable to reach the Home Assistant cloud."),
+ asyncio.TimeoutError: (
+ HTTP_BAD_GATEWAY,
+ "Unable to reach the Home Assistant cloud.",
+ ),
aiohttp.ClientError: (
HTTP_INTERNAL_SERVER_ERROR,
"Error making internal request",
@@ -122,7 +131,7 @@ async def async_setup(hass):
HTTP_BAD_REQUEST,
"An account with the given email already exists.",
),
- auth.Unauthenticated: (401, "Authentication failed."),
+ auth.Unauthenticated: (HTTP_UNAUTHORIZED, "Authentication failed."),
auth.PasswordChangeRequired: (
HTTP_BAD_REQUEST,
"Password change required.",
@@ -177,7 +186,7 @@ def _process_cloud_exception(exc, where):
if err_info is None:
_LOGGER.exception("Unexpected error processing request for %s", where)
- err_info = (502, f"Unexpected error: {exc}")
+ err_info = (HTTP_BAD_GATEWAY, f"Unexpected error: {exc}")
return err_info
diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py
index 6ec92477555586..5f04ba134a8bc3 100644
--- a/homeassistant/components/comfoconnect/sensor.py
+++ b/homeassistant/components/comfoconnect/sensor.py
@@ -34,6 +34,7 @@
TEMP_CELSIUS,
TIME_DAYS,
TIME_HOURS,
+ VOLUME_CUBIC_METERS,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -159,14 +160,14 @@
ATTR_AIR_FLOW_SUPPLY: {
ATTR_DEVICE_CLASS: None,
ATTR_LABEL: "Supply airflow",
- ATTR_UNIT: f"m³/{TIME_HOURS}",
+ ATTR_UNIT: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}",
ATTR_ICON: "mdi:fan",
ATTR_ID: SENSOR_FAN_SUPPLY_FLOW,
},
ATTR_AIR_FLOW_EXHAUST: {
ATTR_DEVICE_CLASS: None,
ATTR_LABEL: "Exhaust airflow",
- ATTR_UNIT: f"m³/{TIME_HOURS}",
+ ATTR_UNIT: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}",
ATTR_ICON: "mdi:fan",
ATTR_ID: SENSOR_FAN_EXHAUST_FLOW,
},
diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py
index 94880dcccf7493..f502e805c853d2 100644
--- a/homeassistant/components/concord232/alarm_control_panel.py
+++ b/homeassistant/components/concord232/alarm_control_panel.py
@@ -101,7 +101,7 @@ def update(self):
except requests.exceptions.ConnectionError as ex:
_LOGGER.error(
"Unable to connect to %(host)s: %(reason)s",
- dict(host=self._url, reason=ex),
+ {"host": self._url, "reason": ex},
)
return
except IndexError:
diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py
index 3077056c397df0..7fd1d9748b337d 100644
--- a/homeassistant/components/concord232/binary_sensor.py
+++ b/homeassistant/components/concord232/binary_sensor.py
@@ -7,6 +7,10 @@
import voluptuous as vol
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_SAFETY,
+ DEVICE_CLASS_SMOKE,
DEVICE_CLASSES,
PLATFORM_SCHEMA,
BinarySensorEntity,
@@ -85,14 +89,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def get_opening_type(zone):
"""Return the result of the type guessing from name."""
if "MOTION" in zone["name"]:
- return "motion"
+ return DEVICE_CLASS_MOTION
if "KEY" in zone["name"]:
- return "safety"
+ return DEVICE_CLASS_SAFETY
if "SMOKE" in zone["name"]:
- return "smoke"
+ return DEVICE_CLASS_SMOKE
if "WATER" in zone["name"]:
return "water"
- return "opening"
+ return DEVICE_CLASS_OPENING
class Concord232ZoneSensor(BinarySensorEntity):
@@ -111,11 +115,6 @@ def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
- @property
- def should_poll(self):
- """No polling needed."""
- return True
-
@property
def name(self):
"""Return the name of the binary sensor."""
diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py
index e26b2b80bc13f1..22a9bf4f02ab8c 100644
--- a/homeassistant/components/config/group.py
+++ b/homeassistant/components/config/group.py
@@ -1,8 +1,14 @@
"""Provide configuration end points for Groups."""
-from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
+from homeassistant.components.group import (
+ DOMAIN,
+ GROUP_SCHEMA,
+ GroupIntegrationRegistry,
+)
from homeassistant.config import GROUP_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
+from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
from . import EditKeyBasedConfigView
@@ -25,3 +31,11 @@ async def hook(action, config_key):
)
)
return True
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ return
diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py
index edc2e9af42c98a..7a99d29c774a6a 100644
--- a/homeassistant/components/config/zwave.py
+++ b/homeassistant/components/config/zwave.py
@@ -6,7 +6,7 @@
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const
-from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK
+from homeassistant.const import HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK
import homeassistant.core as ha
import homeassistant.helpers.config_validation as cv
@@ -254,7 +254,9 @@ def _set_protection():
)
state = node.set_protection(value_id, selection)
if not state:
- return self.json_message("Protection setting did not complete", 202)
+ return self.json_message(
+ "Protection setting did not complete", HTTP_ACCEPTED
+ )
return self.json_message("Protection setting succsessfully set", HTTP_OK)
return await hass.async_add_executor_job(_set_protection)
diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json
new file mode 100644
index 00000000000000..1653a11c3ed996
--- /dev/null
+++ b/homeassistant/components/control4/translations/de.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "IP-Addresse",
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/fr.json b/homeassistant/components/control4/translations/fr.json
index 1b3803499de549..7d9bd88a81076c 100644
--- a/homeassistant/components/control4/translations/fr.json
+++ b/homeassistant/components/control4/translations/fr.json
@@ -14,6 +14,16 @@
"host": "Adresse IP",
"password": "Mot de passe",
"username": "Nom d'utilisateur"
+ },
+ "description": "Veuillez saisir les d\u00e9tails de votre compte Control4 et l'adresse IP de votre contr\u00f4leur local."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Secondes entre les mises \u00e0 jour"
}
}
}
diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/control4/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/nl.json b/homeassistant/components/control4/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/control4/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/pl.json b/homeassistant/components/control4/translations/pl.json
index 3064a0044b1835..0076cfb69dd479 100644
--- a/homeassistant/components/control4/translations/pl.json
+++ b/homeassistant/components/control4/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json
index c771883714a12b..6c0426eba8fb3c 100644
--- a/homeassistant/components/control4/translations/uk.json
+++ b/homeassistant/components/control4/translations/uk.json
@@ -3,7 +3,6 @@
"step": {
"user": {
"data": {
- "password": "",
"username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
}
}
diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py
index 77afd85395abea..7077854a768e2d 100644
--- a/homeassistant/components/coolmaster/climate.py
+++ b/homeassistant/components/coolmaster/climate.py
@@ -14,6 +14,7 @@
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN
@@ -66,15 +67,10 @@ def __init__(self, coordinator, unit_id, unit, supported_modes, info):
self._hvac_modes = supported_modes
self._info = info
- def _refresh_from_coordinator(self):
+ @callback
+ def _handle_coordinator_update(self):
self._unit = self.coordinator.data[self._unit_id]
- self.async_write_ha_state()
-
- async def async_added_to_hass(self):
- """When entity is added to hass."""
- self.async_on_remove(
- self.coordinator.async_add_listener(self._refresh_from_coordinator)
- )
+ super()._handle_coordinator_update()
@property
def device_info(self):
diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py
index c674146fd15697..7c9b4d5d065b6b 100644
--- a/homeassistant/components/coolmaster/config_flow.py
+++ b/homeassistant/components/coolmaster/config_flow.py
@@ -54,7 +54,7 @@ async def async_step_user(self, user_input=None):
if not result:
errors["base"] = "no_units"
except (OSError, ConnectionRefusedError, TimeoutError):
- errors["base"] = "connection_error"
+ errors["base"] = "cannot_connect"
if errors:
return self.async_show_form(
diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json
index 58bd51fca4da17..85bd3b1893febc 100644
--- a/homeassistant/components/coolmaster/manifest.json
+++ b/homeassistant/components/coolmaster/manifest.json
@@ -3,6 +3,6 @@
"name": "CoolMasterNet",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
- "requirements": ["pycoolmasternet-async==0.1.1"],
+ "requirements": ["pycoolmasternet-async==0.1.2"],
"codeowners": ["@OnFreund"]
}
diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json
index fd91e5576d2dca..7afc012a1917b5 100644
--- a/homeassistant/components/coolmaster/strings.json
+++ b/homeassistant/components/coolmaster/strings.json
@@ -15,8 +15,8 @@
}
},
"error": {
- "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_units": "Could not find any HVAC units in CoolMasterNet host."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/coolmaster/translations/ca.json b/homeassistant/components/coolmaster/translations/ca.json
index fb256e52f8824c..40186987031ce5 100644
--- a/homeassistant/components/coolmaster/translations/ca.json
+++ b/homeassistant/components/coolmaster/translations/ca.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb la inst\u00e0ncia de CoolMasterNet. Comprova l'amfitri\u00f3.",
"no_units": "No s'ha pogut trobar cap unitat d'HVAC a l'amfitri\u00f3 de CoolMasterNet."
},
diff --git a/homeassistant/components/coolmaster/translations/el.json b/homeassistant/components/coolmaster/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/translations/en.json b/homeassistant/components/coolmaster/translations/en.json
index 6c09ceb725b94c..3e69c5c82fd61a 100644
--- a/homeassistant/components/coolmaster/translations/en.json
+++ b/homeassistant/components/coolmaster/translations/en.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.",
"no_units": "Could not find any HVAC units in CoolMasterNet host."
},
diff --git a/homeassistant/components/coolmaster/translations/es.json b/homeassistant/components/coolmaster/translations/es.json
index 6835914c5137e9..3d5524d1f135e9 100644
--- a/homeassistant/components/coolmaster/translations/es.json
+++ b/homeassistant/components/coolmaster/translations/es.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "Error al conectarse a la instancia de CoolMasterNet. Por favor revise su anfitri\u00f3n.",
"no_units": "No se ha encontrado ninguna unidad HVAC en el host CoolMasterNet."
},
diff --git a/homeassistant/components/coolmaster/translations/et.json b/homeassistant/components/coolmaster/translations/et.json
new file mode 100644
index 00000000000000..6342c5d2aa2cf3
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "CoolMasterNet'iga \u00fchenduse loomine nurjus. Palun kontrolli hosti andmeid."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/translations/fr.json b/homeassistant/components/coolmaster/translations/fr.json
index f790e0187186bb..2028043dc3e7c0 100644
--- a/homeassistant/components/coolmaster/translations/fr.json
+++ b/homeassistant/components/coolmaster/translations/fr.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "\u00c9chec de la connexion \u00e0 l'instance CoolMasterNet. S'il vous pla\u00eet v\u00e9rifier votre h\u00f4te.",
"no_units": "Impossible de trouver des unit\u00e9s HVAC dans l'h\u00f4te CoolMasterNet."
},
diff --git a/homeassistant/components/coolmaster/translations/it.json b/homeassistant/components/coolmaster/translations/it.json
index 33ac306ce1a04d..0e7814581e30df 100644
--- a/homeassistant/components/coolmaster/translations/it.json
+++ b/homeassistant/components/coolmaster/translations/it.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi all'istanza CoolMasterNet. Controlla il tuo host.",
"no_units": "Impossibile trovare alcuna unit\u00e0 HVAC nell'host CoolMasterNet."
},
diff --git a/homeassistant/components/coolmaster/translations/no.json b/homeassistant/components/coolmaster/translations/no.json
index 328b113a1824f4..c77773fe5c3b84 100644
--- a/homeassistant/components/coolmaster/translations/no.json
+++ b/homeassistant/components/coolmaster/translations/no.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til CoolMasterNet-forekomsten. Sjekk verten din.",
"no_units": "Kunne ikke finne noen HVAC-enheter i CoolMasterNet vert."
},
diff --git a/homeassistant/components/coolmaster/translations/pl.json b/homeassistant/components/coolmaster/translations/pl.json
index 9b0e4bc5846a26..2a15839a86281f 100644
--- a/homeassistant/components/coolmaster/translations/pl.json
+++ b/homeassistant/components/coolmaster/translations/pl.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 z CoolMasterNet. Sprawd\u017a adres hosta.",
"no_units": "Nie mo\u017cna znale\u017a\u0107 urz\u0105dze\u0144 HVAC na ho\u015bcie CoolMasterNet."
},
diff --git a/homeassistant/components/coolmaster/translations/ru.json b/homeassistant/components/coolmaster/translations/ru.json
index 993a66539b2db3..3c508e80ab3790 100644
--- a/homeassistant/components/coolmaster/translations/ru.json
+++ b/homeassistant/components/coolmaster/translations/ru.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "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, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.",
"no_units": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u0438 \u0438 \u043a\u043e\u043d\u0434\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f."
},
diff --git a/homeassistant/components/coolmaster/translations/zh-Hant.json b/homeassistant/components/coolmaster/translations/zh-Hant.json
index a96bf8bd432084..e3d6fc83db7cc4 100644
--- a/homeassistant/components/coolmaster/translations/zh-Hant.json
+++ b/homeassistant/components/coolmaster/translations/zh-Hant.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "\u9023\u7dda\u81f3 CoolMasterNet \u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u3002",
"no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u8a2d\u5099\u3002"
},
diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json
index 949034e6bc7f41..6a5b2626003850 100644
--- a/homeassistant/components/coronavirus/strings.json
+++ b/homeassistant/components/coronavirus/strings.json
@@ -6,6 +6,8 @@
"data": { "country": "Country" }
}
},
- "abort": { "already_configured": "This country is already configured." }
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
+ }
}
}
diff --git a/homeassistant/components/coronavirus/translations/ca.json b/homeassistant/components/coronavirus/translations/ca.json
index c44da0ab21ad6e..82e46a209d02a0 100644
--- a/homeassistant/components/coronavirus/translations/ca.json
+++ b/homeassistant/components/coronavirus/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Aquest pa\u00eds ja est\u00e0 configurat."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/en.json b/homeassistant/components/coronavirus/translations/en.json
index f388c734351344..cbd057bfce109f 100644
--- a/homeassistant/components/coronavirus/translations/en.json
+++ b/homeassistant/components/coronavirus/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "This country is already configured."
+ "already_configured": "Service is already configured"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/et.json b/homeassistant/components/coronavirus/translations/et.json
new file mode 100644
index 00000000000000..a69b845e623c76
--- /dev/null
+++ b/homeassistant/components/coronavirus/translations/et.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "See riik on juba seadistatud."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "Riik"
+ },
+ "title": "Vali j\u00e4lgiv riik"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coronavirus/translations/it.json b/homeassistant/components/coronavirus/translations/it.json
index 26b40e06ebd704..8cc2065b94a641 100644
--- a/homeassistant/components/coronavirus/translations/it.json
+++ b/homeassistant/components/coronavirus/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Questa Nazione \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/no.json b/homeassistant/components/coronavirus/translations/no.json
index 359f15b3323450..bf111868e4bf18 100644
--- a/homeassistant/components/coronavirus/translations/no.json
+++ b/homeassistant/components/coronavirus/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Dette landet er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/ru.json b/homeassistant/components/coronavirus/translations/ru.json
index 7a39c547c82ba5..e7e6798f6a4f68 100644
--- a/homeassistant/components/coronavirus/translations/ru.json
+++ b/homeassistant/components/coronavirus/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py
new file mode 100644
index 00000000000000..d031b7cf693c29
--- /dev/null
+++ b/homeassistant/components/cover/group.py
@@ -0,0 +1,16 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_CLOSED, STATE_OPEN
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ # On means open, Off means closed
+ registry.on_off_states({STATE_OPEN}, STATE_CLOSED)
diff --git a/homeassistant/components/cover/translations/et.json b/homeassistant/components/cover/translations/et.json
index 96d81b3a7b61a2..baca6feeca5044 100644
--- a/homeassistant/components/cover/translations/et.json
+++ b/homeassistant/components/cover/translations/et.json
@@ -1,12 +1,39 @@
{
+ "device_automation": {
+ "action_type": {
+ "close": "Sule aknakate {entity_name}",
+ "close_tilt": "Sule aknakatte {entity_name} kaldribid",
+ "open": "Ava aknakate {entity_name}",
+ "open_tilt": "Ava aknakatte {entity_name} kaldribid",
+ "set_position": "M\u00e4\u00e4ra aknakatte {entity_name} asend",
+ "set_tilt_position": "M\u00e4\u00e4ra aknakatte {entity_name} kaldribide asend",
+ "stop": "Peata aknakatte {entity_name} liikumine"
+ },
+ "condition_type": {
+ "is_closed": "Aknakate {entity_name} on suletud",
+ "is_closing": "Aknakate {entity_name} sulgub",
+ "is_open": "Aknakate {entity_name} on avatud",
+ "is_opening": "Aknakate {entity_name} avaneb",
+ "is_position": "Aknakatte {entity_name} praegune asend on",
+ "is_tilt_position": "Aknakatte {entity_name} praegune kalle on"
+ },
+ "trigger_type": {
+ "closed": "Aknakate {entity_name} sulgus",
+ "closing": "Aknakate {entity_name} sulgub",
+ "opened": "Aknakate {entity_name} avanes",
+ "opening": "Aknakate {entity_name} avaneb",
+ "position": "Aknakatte {entity_name} asend muutub",
+ "tilt_position": "Aknakatte {entity_name} kalle muutub"
+ }
+ },
"state": {
"_": {
"closed": "Suletud",
- "closing": "Sulgub",
+ "closing": "Aknakate sulgub",
"open": "Avatud",
"opening": "Avaneb",
- "stopped": "Peatatud"
+ "stopped": "Aknakate peatatus"
}
},
- "title": "Kate"
+ "title": "Kardin"
}
\ No newline at end of file
diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json
index 0485a9bb37198b..66cd0c77c73c97 100644
--- a/homeassistant/components/cover/translations/uk.json
+++ b/homeassistant/components/cover/translations/uk.json
@@ -2,6 +2,9 @@
"device_automation": {
"action_type": {
"stop": "\u0417\u0443\u043f\u0438\u043d\u0438\u0442\u0438 {entity_name}"
+ },
+ "trigger_type": {
+ "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e"
}
},
"state": {
diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json
index fc39f78c2d0f81..7b40fa55a1d673 100644
--- a/homeassistant/components/daikin/translations/pl.json
+++ b/homeassistant/components/daikin/translations/pl.json
@@ -1,12 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"error": {
- "device_fail": "Nieoczekiwany b\u0142\u0105d.",
- "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "forbidden": "Niepoprawne uwierzytelnienie."
+ "device_fail": "Nieoczekiwany b\u0142\u0105d",
+ "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "forbidden": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py
index 7f6876a709be48..9d3123185c4f9d 100644
--- a/homeassistant/components/danfoss_air/binary_sensor.py
+++ b/homeassistant/components/danfoss_air/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for the for Danfoss Air HRV binary sensors."""
from pydanfossair.commands import ReadCommand
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OPENING,
+ BinarySensorEntity,
+)
from . import DOMAIN as DANFOSS_AIR_DOMAIN
@@ -11,7 +14,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
- ["Danfoss Air Bypass Active", ReadCommand.bypass, "opening"],
+ ["Danfoss Air Bypass Active", ReadCommand.bypass, DEVICE_CLASS_OPENING],
["Danfoss Air Away Mode Active", ReadCommand.away_mode, None],
]
diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index e167f16b4e44b1..49b5c6c69f6ec8 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -18,7 +18,9 @@
DEGREE,
LENGTH_CENTIMETERS,
LENGTH_KILOMETERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
+ PRESSURE_MBAR,
SPEED_KILOMETERS_PER_HOUR,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
@@ -109,11 +111,11 @@
],
"precip_intensity": [
"Precip Intensity",
- f"mm/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"in",
- f"mm/{TIME_HOURS}",
- f"mm/{TIME_HOURS}",
- f"mm/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"mdi:weather-rainy",
["currently", "minutely", "hourly", "daily"],
],
@@ -219,11 +221,11 @@
],
"pressure": [
"Pressure",
- "mbar",
- "mbar",
- "mbar",
- "mbar",
- "mbar",
+ PRESSURE_MBAR,
+ PRESSURE_MBAR,
+ PRESSURE_MBAR,
+ PRESSURE_MBAR,
+ PRESSURE_MBAR,
"mdi:gauge",
["currently", "hourly", "daily"],
],
@@ -329,11 +331,11 @@
],
"precip_intensity_max": [
"Daily Max Precip Intensity",
- f"mm/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"in",
- f"mm/{TIME_HOURS}",
- f"mm/{TIME_HOURS}",
- f"mm/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"mdi:thermometer",
["daily"],
],
diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py
index 9b6fd1bdb64300..c324d6a5b6469d 100644
--- a/homeassistant/components/ddwrt/device_tracker.py
+++ b/homeassistant/components/ddwrt/device_tracker.py
@@ -17,6 +17,7 @@
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
)
import homeassistant.helpers.config_validation as cv
@@ -155,7 +156,7 @@ def get_ddwrt_data(self, url):
return
if response.status_code == HTTP_OK:
return _parse_ddwrt_response(response.text)
- if response.status_code == 401:
+ if response.status_code == HTTP_UNAUTHORIZED:
# Authentication error
_LOGGER.exception(
"Failed to authenticate, check your username and password"
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index c8917934dd7b20..a9f49586134583 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -8,6 +8,7 @@
DEVICE_CLASS_OPENING,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_VIBRATION,
+ DOMAIN,
BinarySensorEntity,
)
from homeassistant.const import ATTR_TEMPERATURE
@@ -32,24 +33,21 @@
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
@callback
- def async_add_sensor(sensors, new=True):
+ def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
entities = []
for sensor in sensors:
if (
- new
- and sensor.BINARY
+ sensor.BINARY
+ and sensor.uniqueid not in gateway.entities[DOMAIN]
and (
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
@@ -57,7 +55,8 @@ def async_add_sensor(sensors, new=True):
):
entities.append(DeconzBinarySensor(sensor, gateway))
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -73,15 +72,14 @@ def async_add_sensor(sensors, new=True):
class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
"""Representation of a deCONZ binary sensor."""
+ TYPE = DOMAIN
+
@callback
- def async_update_callback(self, force_update=False, ignore_update=False):
+ def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
- if ignore_update:
- return
-
keys = {"on", "reachable", "state"}
if force_update or self._device.changed_keys.intersection(keys):
- self.async_write_ha_state()
+ super().async_update_callback(force_update=force_update)
@property
def is_on(self):
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index 424693505ca47f..e4de5badb61e39 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -1,7 +1,7 @@
"""Support for deCONZ climate devices."""
from pydeconz.sensor import Thermostat
-from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate import DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
@@ -19,27 +19,24 @@
SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ climate devices.
Thermostats are based on the same device class as sensors in deCONZ.
"""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
@callback
- def async_add_climate(sensors, new=True):
+ def async_add_climate(sensors):
"""Add climate devices from deCONZ."""
entities = []
for sensor in sensors:
if (
- new
- and sensor.type in Thermostat.ZHATYPE
+ sensor.type in Thermostat.ZHATYPE
+ and sensor.uniqueid not in gateway.entities[DOMAIN]
and (
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
@@ -47,7 +44,8 @@ def async_add_climate(sensors, new=True):
):
entities.append(DeconzThermostat(sensor, gateway))
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -61,6 +59,8 @@ def async_add_climate(sensors, new=True):
class DeconzThermostat(DeconzDevice, ClimateEntity):
"""Representation of a deCONZ thermostat."""
+ TYPE = DOMAIN
+
@property
def supported_features(self):
"""Return the list of supported features."""
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index f3ae5682131890..c43c1c955048e1 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -22,13 +22,13 @@
from .const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
+ CONF_ALLOW_NEW_DEVICES,
CONF_BRIDGE_ID,
- DEFAULT_ALLOW_CLIP_SENSOR,
- DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)
+from .gateway import get_gateway_from_config_entry
DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de"
CONF_SERIAL = "serial"
@@ -172,6 +172,9 @@ async def _create_entry(self):
except asyncio.TimeoutError:
return self.async_abort(reason="no_bridges")
+ if self.bridge_id == "0000000000000000":
+ return self.async_abort(reason="no_hardware_available")
+
return self.async_create_entry(title=self.bridge_id, data=self.deconz_config)
async def async_step_ssdp(self, discovery_info):
@@ -251,18 +254,17 @@ def __init__(self, config_entry):
"""Initialize deCONZ options flow."""
self.config_entry = config_entry
self.options = dict(config_entry.options)
+ self.gateway = None
async def async_step_init(self, user_input=None):
"""Manage the deCONZ options."""
+ self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry)
return await self.async_step_deconz_devices()
async def async_step_deconz_devices(self, user_input=None):
"""Manage the deconz devices options."""
if user_input is not None:
- self.options[CONF_ALLOW_CLIP_SENSOR] = user_input[CONF_ALLOW_CLIP_SENSOR]
- self.options[CONF_ALLOW_DECONZ_GROUPS] = user_input[
- CONF_ALLOW_DECONZ_GROUPS
- ]
+ self.options.update(user_input)
return self.async_create_entry(title="", data=self.options)
return self.async_show_form(
@@ -271,15 +273,15 @@ async def async_step_deconz_devices(self, user_input=None):
{
vol.Optional(
CONF_ALLOW_CLIP_SENSOR,
- default=self.config_entry.options.get(
- CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
- ),
+ default=self.gateway.option_allow_clip_sensor,
): bool,
vol.Optional(
CONF_ALLOW_DECONZ_GROUPS,
- default=self.config_entry.options.get(
- CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
- ),
+ default=self.gateway.option_allow_deconz_groups,
+ ): bool,
+ vol.Optional(
+ CONF_ALLOW_NEW_DEVICES,
+ default=self.gateway.option_allow_new_devices,
): bool,
}
),
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index c2190321fdf837..d965b6485f8a81 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -1,6 +1,15 @@
"""Constants for the deCONZ component."""
import logging
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
+from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
+from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+
LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
@@ -11,19 +20,22 @@
DEFAULT_PORT = 80
DEFAULT_ALLOW_CLIP_SENSOR = False
DEFAULT_ALLOW_DECONZ_GROUPS = True
+DEFAULT_ALLOW_NEW_DEVICES = True
CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor"
CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups"
+CONF_ALLOW_NEW_DEVICES = "allow_new_devices"
CONF_MASTER_GATEWAY = "master"
SUPPORTED_PLATFORMS = [
- "binary_sensor",
- "climate",
- "cover",
- "light",
- "scene",
- "sensor",
- "switch",
+ BINARY_SENSOR_DOMAIN,
+ CLIMATE_DOMAIN,
+ COVER_DOMAIN,
+ LIGHT_DOMAIN,
+ LOCK_DOMAIN,
+ SCENE_DOMAIN,
+ SENSOR_DOMAIN,
+ SWITCH_DOMAIN,
]
NEW_GROUP = "groups"
@@ -36,12 +48,20 @@
ATTR_ON = "on"
ATTR_VALVE = "valve"
+# Covers
DAMPERS = ["Level controllable output"]
WINDOW_COVERS = ["Window covering device", "Window covering controller"]
COVER_TYPES = DAMPERS + WINDOW_COVERS
+# Locks
+LOCKS = ["Door Lock"]
+LOCK_TYPES = LOCKS
+
+# Switches
POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS
+CONF_ANGLE = "angle"
CONF_GESTURE = "gesture"
+CONF_XY = "xy"
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index e01cfdbe5f8964..82f471305e9f5b 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -1,6 +1,8 @@
"""Support for deCONZ covers."""
from homeassistant.components.cover import (
ATTR_POSITION,
+ DEVICE_CLASS_WINDOW,
+ DOMAIN,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
@@ -15,16 +17,13 @@
from .gateway import get_gateway_from_config_entry
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up covers for deCONZ component.
- Covers are based on same device class as lights in deCONZ.
+ Covers are based on the same device class as lights in deCONZ.
"""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
@callback
def async_add_cover(lights):
@@ -32,10 +31,14 @@ def async_add_cover(lights):
entities = []
for light in lights:
- if light.type in COVER_TYPES:
+ if (
+ light.type in COVER_TYPES
+ and light.uniqueid not in gateway.entities[DOMAIN]
+ ):
entities.append(DeconzCover(light, gateway))
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -49,6 +52,8 @@ def async_add_cover(lights):
class DeconzCover(DeconzDevice, CoverEntity):
"""Representation of a deCONZ cover."""
+ TYPE = DOMAIN
+
def __init__(self, device, gateway):
"""Set up cover device."""
super().__init__(device, gateway)
@@ -74,7 +79,7 @@ def device_class(self):
if self._device.type in DAMPERS:
return "damper"
if self._device.type in WINDOW_COVERS:
- return "window"
+ return DEVICE_CLASS_WINDOW
@property
def supported_features(self):
diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py
index b77014cc34bb4c..4bcd63c8fa227f 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -14,7 +14,6 @@ def __init__(self, device, gateway):
"""Set up device and add update callback to get data from websocket."""
self._device = device
self.gateway = gateway
- self.listeners = []
@property
def unique_id(self):
@@ -51,11 +50,12 @@ def device_info(self):
class DeconzDevice(DeconzBase, Entity):
"""Representation of a deCONZ device."""
+ TYPE = ""
+
def __init__(self, device, gateway):
"""Set up device and add update callback to get data from websocket."""
super().__init__(device, gateway)
-
- self.unsub_dispatcher = None
+ self.gateway.entities[self.TYPE].add(self.unique_id)
@property
def entity_registry_enabled_default(self):
@@ -72,7 +72,7 @@ async def async_added_to_hass(self):
"""Subscribe to device events."""
self._device.register_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id
- self.listeners.append(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass, self.gateway.signal_reachable, self.async_update_callback
)
@@ -83,13 +83,12 @@ async def async_will_remove_from_hass(self) -> None:
self._device.remove_callback(self.async_update_callback)
if self.entity_id in self.gateway.deconz_ids:
del self.gateway.deconz_ids[self.entity_id]
- for unsub_dispatcher in self.listeners:
- unsub_dispatcher()
+ self.gateway.entities[self.TYPE].remove(self.unique_id)
@callback
- def async_update_callback(self, force_update=False, ignore_update=False):
+ def async_update_callback(self, force_update=False):
"""Update the device's state."""
- if ignore_update:
+ if not force_update and self.gateway.ignore_state_updates:
return
self.async_write_ha_state()
diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py
index 9ad4a7f3162f6d..968ab3cee391b5 100644
--- a/homeassistant/components/deconz/deconz_event.py
+++ b/homeassistant/components/deconz/deconz_event.py
@@ -1,14 +1,57 @@
"""Representation of a deCONZ remote."""
+from pydeconz.sensor import Switch
+
from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
-from .const import CONF_GESTURE, LOGGER
+from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER, NEW_SENSOR
from .deconz_device import DeconzBase
CONF_DECONZ_EVENT = "deconz_event"
+async def async_setup_events(gateway) -> None:
+ """Set up the deCONZ events."""
+
+ @callback
+ def async_add_sensor(sensors):
+ """Create DeconzEvent."""
+ for sensor in sensors:
+
+ if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
+ continue
+
+ if sensor.type not in Switch.ZHATYPE or sensor.uniqueid in {
+ event.unique_id for event in gateway.events
+ }:
+ continue
+
+ new_event = DeconzEvent(sensor, gateway)
+ gateway.hass.async_create_task(new_event.async_update_device_registry())
+ gateway.events.append(new_event)
+
+ gateway.listeners.append(
+ async_dispatcher_connect(
+ gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor
+ )
+ )
+
+ async_add_sensor(
+ [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)]
+ )
+
+
+@callback
+def async_unload_events(gateway) -> None:
+ """Unload all deCONZ events."""
+ for event in gateway.events:
+ event.async_will_remove_from_hass()
+
+ gateway.events.clear()
+
+
class DeconzEvent(DeconzBase):
"""When you want signals instead of entities.
@@ -35,12 +78,14 @@ def device(self):
def async_will_remove_from_hass(self) -> None:
"""Disconnect event object when removed."""
self._device.remove_callback(self.async_update_callback)
- self._device = None
@callback
- def async_update_callback(self, force_update=False, ignore_update=False):
+ def async_update_callback(self, force_update=False):
"""Fire the event if reason is that state is updated."""
- if ignore_update or "state" not in self._device.changed_keys:
+ if (
+ self.gateway.ignore_state_updates
+ or "state" not in self._device.changed_keys
+ ):
return
data = {
@@ -52,6 +97,12 @@ def async_update_callback(self, force_update=False, ignore_update=False):
if self._device.gesture is not None:
data[CONF_GESTURE] = self._device.gesture
+ if self._device.angle is not None:
+ data[CONF_ANGLE] = self._device.angle
+
+ if self._device.xy is not None:
+ data[CONF_XY] = self._device.xy
+
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
async def async_update_device_registry(self):
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index bddde34a7ded0c..e400afd9b9f3d9 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -16,6 +16,7 @@
)
from . import DOMAIN
+from .const import LOGGER
from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE
CONF_SUBTYPE = "subtype"
@@ -267,7 +268,8 @@
}
AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01"
-AQARA_SINGLE_WALL_SWITCH_WXKG03LM = {
+AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL = "lumi.remote.b186acn02"
+AQARA_SINGLE_WALL_SWITCH = {
(CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002},
(CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
(CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004},
@@ -370,7 +372,8 @@
AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH,
AQARA_DOUBLE_WALL_SWITCH_MODEL_2020: AQARA_DOUBLE_WALL_SWITCH,
AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM,
- AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH_WXKG03LM,
+ AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH,
+ AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL: AQARA_SINGLE_WALL_SWITCH,
AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH,
AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH,
AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH,
@@ -427,6 +430,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
deconz_event = _get_deconz_event_from_device_id(hass, device.id)
if deconz_event is None:
+ LOGGER.error("No deconz_event tied to device %s found", device.name)
raise InvalidDeviceAutomationConfig
event_id = deconz_event.serial
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 828f65c9811154..5ec07c40754772 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -14,9 +14,11 @@
from .const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
+ CONF_ALLOW_NEW_DEVICES,
CONF_MASTER_GATEWAY,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
+ DEFAULT_ALLOW_NEW_DEVICES,
DOMAIN,
LOGGER,
NEW_GROUP,
@@ -25,6 +27,7 @@
NEW_SENSOR,
SUPPORTED_PLATFORMS,
)
+from .deconz_event import async_setup_events, async_unload_events
from .errors import AuthenticationRequired, CannotConnect
@@ -42,9 +45,14 @@ def __init__(self, hass, config_entry) -> None:
self.hass = hass
self.config_entry = config_entry
- self.available = True
self.api = None
+
+ self.available = True
+ self.ignore_state_updates = False
+
self.deconz_ids = {}
+ self.device_id = None
+ self.entities = {}
self.events = []
self.listeners = []
@@ -56,11 +64,18 @@ def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
return self.config_entry.unique_id
+ @property
+ def host(self) -> str:
+ """Return the host of the gateway."""
+ return self.config_entry.data[CONF_HOST]
+
@property
def master(self) -> bool:
"""Gateway which is used with deCONZ services without defining id."""
return self.config_entry.options[CONF_MASTER_GATEWAY]
+ # Options
+
@property
def option_allow_clip_sensor(self) -> bool:
"""Allow loading clip sensor from gateway."""
@@ -75,10 +90,57 @@ def option_allow_deconz_groups(self) -> bool:
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
)
+ @property
+ def option_allow_new_devices(self) -> bool:
+ """Allow automatic adding of new devices."""
+ return self.config_entry.options.get(
+ CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES
+ )
+
+ # Signals
+
+ @property
+ def signal_reachable(self) -> str:
+ """Gateway specific event to signal a change in connection status."""
+ return f"deconz-reachable-{self.bridgeid}"
+
+ @callback
+ def async_signal_new_device(self, device_type) -> str:
+ """Gateway specific event to signal new device."""
+ new_device = {
+ NEW_GROUP: f"deconz_new_group_{self.bridgeid}",
+ NEW_LIGHT: f"deconz_new_light_{self.bridgeid}",
+ NEW_SCENE: f"deconz_new_scene_{self.bridgeid}",
+ NEW_SENSOR: f"deconz_new_sensor_{self.bridgeid}",
+ }
+ return new_device[device_type]
+
+ # Callbacks
+
+ @callback
+ def async_connection_status_callback(self, available) -> None:
+ """Handle signals of gateway connection status."""
+ self.available = available
+ self.ignore_state_updates = False
+ async_dispatcher_send(self.hass, self.signal_reachable, True)
+
+ @callback
+ def async_add_device_callback(self, device_type, device) -> None:
+ """Handle event of new device creation in deCONZ."""
+ if not self.option_allow_new_devices:
+ return
+
+ if not isinstance(device, list):
+ device = [device]
+
+ async_dispatcher_send(
+ self.hass, self.async_signal_new_device(device_type), device
+ )
+
async def async_update_device_registry(self) -> None:
"""Update device registry."""
device_registry = await self.hass.helpers.device_registry.async_get_registry()
- device_registry.async_get_or_create(
+ entry = device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)},
identifiers={(DOMAIN, self.api.config.bridgeid)},
@@ -87,6 +149,7 @@ async def async_update_device_registry(self) -> None:
name=self.api.config.name,
sw_version=self.api.config.swversion,
)
+ self.device_id = entry.id
async def async_setup(self) -> bool:
"""Set up a deCONZ gateway."""
@@ -112,6 +175,8 @@ async def async_setup(self) -> bool:
)
)
+ self.hass.async_create_task(async_setup_events(self))
+
self.api.start()
self.config_entry.add_update_listener(self.async_config_entry_updated)
@@ -126,11 +191,13 @@ async def async_config_entry_updated(hass, entry) -> None:
Causes for this is either discovery updating host address or config entry options changing.
"""
gateway = get_gateway_from_config_entry(hass, entry)
+
if not gateway:
return
- if gateway.api.host != entry.data[CONF_HOST]:
+
+ if gateway.api.host != gateway.host:
gateway.api.close()
- gateway.api.host = entry.data[CONF_HOST]
+ gateway.api.host = gateway.host
gateway.api.start()
return
@@ -174,37 +241,6 @@ async def options_updated(self):
# from Home Assistant
entity_registry.async_remove(entity_id)
- @property
- def signal_reachable(self) -> str:
- """Gateway specific event to signal a change in connection status."""
- return f"deconz-reachable-{self.bridgeid}"
-
- @callback
- def async_connection_status_callback(self, available) -> None:
- """Handle signals of gateway connection status."""
- self.available = available
- async_dispatcher_send(self.hass, self.signal_reachable, True)
-
- @callback
- def async_signal_new_device(self, device_type) -> str:
- """Gateway specific event to signal new device."""
- new_device = {
- NEW_GROUP: f"deconz_new_group_{self.bridgeid}",
- NEW_LIGHT: f"deconz_new_light_{self.bridgeid}",
- NEW_SCENE: f"deconz_new_scene_{self.bridgeid}",
- NEW_SENSOR: f"deconz_new_sensor_{self.bridgeid}",
- }
- return new_device[device_type]
-
- @callback
- def async_add_device_callback(self, device_type, device) -> None:
- """Handle event of new device creation in deCONZ."""
- if not isinstance(device, list):
- device = [device]
- async_dispatcher_send(
- self.hass, self.async_signal_new_device(device_type), device
- )
-
@callback
def shutdown(self, event) -> None:
"""Wrap the call to deconz.close.
@@ -227,9 +263,7 @@ async def async_reset(self):
unsub_dispatcher()
self.listeners = []
- for event in self.events:
- event.async_will_remove_from_hass()
- self.events.clear()
+ async_unload_events(self)
self.deconz_ids = {}
return True
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index fc0c01b30df142..50fb3a8300b83c 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -6,6 +6,7 @@
ATTR_FLASH,
ATTR_HS_COLOR,
ATTR_TRANSITION,
+ DOMAIN,
EFFECT_COLORLOOP,
FLASH_LONG,
FLASH_SHORT,
@@ -25,6 +26,7 @@
CONF_GROUP_ID_BASE,
COVER_TYPES,
DOMAIN as DECONZ_DOMAIN,
+ LOCK_TYPES,
NEW_GROUP,
NEW_LIGHT,
SWITCH_TYPES,
@@ -33,13 +35,10 @@
from .gateway import get_gateway_from_config_entry
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
@callback
def async_add_light(lights):
@@ -47,10 +46,14 @@ def async_add_light(lights):
entities = []
for light in lights:
- if light.type not in COVER_TYPES + SWITCH_TYPES:
+ if (
+ light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
+ and light.uniqueid not in gateway.entities[DOMAIN]
+ ):
entities.append(DeconzLight(light, gateway))
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -67,10 +70,16 @@ def async_add_group(groups):
entities = []
for group in groups:
- if group.lights:
- entities.append(DeconzGroup(group, gateway))
+ if not group.lights:
+ continue
+
+ known_groups = set(gateway.entities[DOMAIN])
+ new_group = DeconzGroup(group, gateway)
+ if new_group.unique_id not in known_groups:
+ entities.append(new_group)
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -85,6 +94,8 @@ def async_add_group(groups):
class DeconzBaseLight(DeconzDevice, LightEntity):
"""Representation of a deCONZ light."""
+ TYPE = DOMAIN
+
def __init__(self, device, gateway):
"""Set up light."""
super().__init__(device, gateway)
@@ -198,10 +209,7 @@ async def async_turn_off(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attributes = {}
- attributes["is_deconz_group"] = self._device.type == "LightGroup"
-
- return attributes
+ return {"is_deconz_group": self._device.type == "LightGroup"}
class DeconzLight(DeconzBaseLight):
@@ -223,13 +231,12 @@ class DeconzGroup(DeconzBaseLight):
def __init__(self, device, gateway):
"""Set up group and create an unique id."""
- super().__init__(device, gateway)
+ group_id_base = gateway.config_entry.unique_id
+ if CONF_GROUP_ID_BASE in gateway.config_entry.data:
+ group_id_base = gateway.config_entry.data[CONF_GROUP_ID_BASE]
+ self._unique_id = f"{group_id_base}-{device.deconz_id}"
- group_id_base = self.gateway.config_entry.unique_id
- if CONF_GROUP_ID_BASE in self.gateway.config_entry.data:
- group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE]
-
- self._unique_id = f"{group_id_base}-{self._device.deconz_id}"
+ super().__init__(device, gateway)
@property
def unique_id(self):
diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py
new file mode 100644
index 00000000000000..1f4fbe570698b5
--- /dev/null
+++ b/homeassistant/components/deconz/lock.py
@@ -0,0 +1,59 @@
+"""Support for deCONZ locks."""
+from homeassistant.components.lock import DOMAIN, LockEntity
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import LOCKS, NEW_LIGHT
+from .deconz_device import DeconzDevice
+from .gateway import get_gateway_from_config_entry
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up locks for deCONZ component.
+
+ Locks are based on the same device class as lights in deCONZ.
+ """
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
+
+ @callback
+ def async_add_lock(lights):
+ """Add lock from deCONZ."""
+ entities = []
+
+ for light in lights:
+
+ if light.type in LOCKS and light.uniqueid not in gateway.entities[DOMAIN]:
+ entities.append(DeconzLock(light, gateway))
+
+ if entities:
+ async_add_entities(entities, True)
+
+ gateway.listeners.append(
+ async_dispatcher_connect(
+ hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock
+ )
+ )
+
+ async_add_lock(gateway.api.lights.values())
+
+
+class DeconzLock(DeconzDevice, LockEntity):
+ """Representation of a deCONZ lock."""
+
+ TYPE = DOMAIN
+
+ @property
+ def is_locked(self):
+ """Return true if lock is on."""
+ return self._device.state
+
+ async def async_lock(self, **kwargs):
+ """Lock the lock."""
+ data = {"on": True}
+ await self._device.async_set_state(data)
+
+ async def async_unlock(self, **kwargs):
+ """Unlock the lock."""
+ data = {"on": False}
+ await self._device.async_set_state(data)
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 2cba87f74d6e0f..6a47864375e919 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
- "requirements": ["pydeconz==72"],
+ "requirements": ["pydeconz==73"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics"
diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py
index fdeb1d43accad1..9ca7f39f034c9b 100644
--- a/homeassistant/components/deconz/scene.py
+++ b/homeassistant/components/deconz/scene.py
@@ -9,10 +9,6 @@
from .gateway import get_gateway_from_config_entry
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up scenes for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
@@ -22,7 +18,8 @@ def async_add_scene(scenes):
"""Add scene from deCONZ."""
entities = [DeconzScene(scene, gateway) for scene in scenes]
- async_add_entities(entities)
+ if entities:
+ async_add_entities(entities)
gateway.listeners.append(
async_dispatcher_connect(
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 4ebed981e2d759..ebc62aee57f5eb 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -12,6 +12,7 @@
Thermostat,
)
+from homeassistant.components.sensor import DOMAIN
from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
@@ -22,6 +23,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_HPA,
@@ -35,7 +37,6 @@
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
-from .deconz_event import DeconzEvent
from .gateway import get_gateway_from_config_entry
ATTR_CURRENT = "current"
@@ -60,67 +61,55 @@
UNIT_OF_MEASUREMENT = {
Consumption: ENERGY_KILO_WATT_HOUR,
Humidity: PERCENTAGE,
- LightLevel: "lx",
+ LightLevel: LIGHT_LUX,
Power: POWER_WATT,
Pressure: PRESSURE_HPA,
Temperature: TEMP_CELSIUS,
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ sensors."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
- batteries = set()
battery_handler = DeconzBatteryHandler(gateway)
@callback
- def async_add_sensor(sensors, new=True):
+ def async_add_sensor(sensors):
"""Add sensors from deCONZ.
- Create DeconzEvent if part of ZHAType list.
- Create DeconzSensor if not a ZHAType and not a binary sensor.
Create DeconzBattery if sensor has a battery attribute.
- If new is false it means an existing sensor has got a battery state reported.
+ Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor.
"""
entities = []
for sensor in sensors:
- if new and sensor.type in Switch.ZHATYPE:
-
- if gateway.option_allow_clip_sensor or not sensor.type.startswith(
- "CLIP"
- ):
- new_event = DeconzEvent(sensor, gateway)
- hass.async_create_task(new_event.async_update_device_registry())
- gateway.events.append(new_event)
-
- elif (
- new
- and sensor.BINARY is False
- and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE
- and (
- gateway.option_allow_clip_sensor
- or not sensor.type.startswith("CLIP")
- )
- ):
- entities.append(DeconzSensor(sensor, gateway))
+ if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
+ continue
if sensor.battery is not None:
+ battery_handler.remove_tracker(sensor)
+
+ known_batteries = set(gateway.entities[DOMAIN])
new_battery = DeconzBattery(sensor, gateway)
- if new_battery.unique_id not in batteries:
- batteries.add(new_battery.unique_id)
+ if new_battery.unique_id not in known_batteries:
entities.append(new_battery)
- battery_handler.remove_tracker(sensor)
+
else:
battery_handler.create_tracker(sensor)
- async_add_entities(entities, True)
+ if (
+ not sensor.BINARY
+ and sensor.type
+ not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE
+ and sensor.uniqueid not in gateway.entities[DOMAIN]
+ ):
+ entities.append(DeconzSensor(sensor, gateway))
+
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -136,15 +125,14 @@ def async_add_sensor(sensors, new=True):
class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
+ TYPE = DOMAIN
+
@callback
- def async_update_callback(self, force_update=False, ignore_update=False):
+ def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
- if ignore_update:
- return
-
keys = {"on", "reachable", "state"}
if force_update or self._device.changed_keys.intersection(keys):
- self.async_write_ha_state()
+ super().async_update_callback(force_update=force_update)
@property
def state(self):
@@ -201,15 +189,14 @@ def device_state_attributes(self):
class DeconzBattery(DeconzDevice):
"""Battery class for when a device is only represented as an event."""
+ TYPE = DOMAIN
+
@callback
- def async_update_callback(self, force_update=False, ignore_update=False):
+ def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
- if ignore_update:
- return
-
keys = {"battery", "reachable"}
if force_update or self._device.changed_keys.intersection(keys):
- self.async_write_ha_state()
+ super().async_update_callback(force_update=force_update)
@property
def unique_id(self):
@@ -273,7 +260,6 @@ def async_update_callback(self, ignore_update=False):
self.gateway.hass,
self.gateway.async_signal_new_device(NEW_SENSOR),
[self.sensor],
- False,
)
diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py
index c85fa8073a3351..bf503376321c1a 100644
--- a/homeassistant/components/deconz/services.py
+++ b/homeassistant/components/deconz/services.py
@@ -3,6 +3,10 @@
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity_registry import (
+ async_entries_for_config_entry,
+ async_entries_for_device,
+)
from .config_flow import get_master_gateway
from .const import (
@@ -35,7 +39,8 @@
)
SERVICE_DEVICE_REFRESH = "device_refresh"
-SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
+SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries"
+SELECT_GATEWAY_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
async def async_setup_services(hass):
@@ -56,6 +61,9 @@ async def async_call_deconz_service(service_call):
elif service == SERVICE_DEVICE_REFRESH:
await async_refresh_devices_service(hass, service_data)
+ elif service == SERVICE_REMOVE_ORPHANED_ENTRIES:
+ await async_remove_orphaned_entries_service(hass, service_data)
+
hass.services.async_register(
DOMAIN,
SERVICE_CONFIGURE_DEVICE,
@@ -67,7 +75,14 @@ async def async_call_deconz_service(service_call):
DOMAIN,
SERVICE_DEVICE_REFRESH,
async_call_deconz_service,
- schema=SERVICE_DEVICE_REFRESH_SCHEMA,
+ schema=SELECT_GATEWAY_SCHEMA,
+ )
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_REMOVE_ORPHANED_ENTRIES,
+ async_call_deconz_service,
+ schema=SELECT_GATEWAY_SCHEMA,
)
@@ -80,6 +95,7 @@ async def async_unload_services(hass):
hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
+ hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES)
async def async_configure_service(hass, data):
@@ -127,7 +143,9 @@ async def async_refresh_devices_service(hass, data):
scenes = set(gateway.api.scenes.keys())
sensors = set(gateway.api.sensors.keys())
- await gateway.api.refresh_state(ignore_update=True)
+ gateway.ignore_state_updates = True
+ await gateway.api.refresh_state()
+ gateway.ignore_state_updates = False
gateway.async_add_device_callback(
NEW_GROUP,
@@ -164,3 +182,54 @@ async def async_refresh_devices_service(hass, data):
if sensor_id not in sensors
],
)
+
+
+async def async_remove_orphaned_entries_service(hass, data):
+ """Remove orphaned deCONZ entries from device and entity registries."""
+ gateway = get_master_gateway(hass)
+ if CONF_BRIDGE_ID in data:
+ gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+
+ entity_entries = async_entries_for_config_entry(
+ entity_registry, gateway.config_entry.entry_id
+ )
+
+ entities_to_be_removed = []
+ devices_to_be_removed = [
+ entry.id
+ for entry in device_registry.devices.values()
+ if gateway.config_entry.entry_id in entry.config_entries
+ ]
+
+ # Don't remove the Gateway device
+ if gateway.device_id in devices_to_be_removed:
+ devices_to_be_removed.remove(gateway.device_id)
+
+ # Don't remove devices belonging to available events
+ for event in gateway.events:
+ if event.device_id in devices_to_be_removed:
+ devices_to_be_removed.remove(event.device_id)
+
+ for entry in entity_entries:
+
+ # Don't remove available entities
+ if entry.unique_id in gateway.entities[entry.domain]:
+
+ # Don't remove devices with available entities
+ if entry.device_id in devices_to_be_removed:
+ devices_to_be_removed.remove(entry.device_id)
+ continue
+ # Remove entities that are not available
+ entities_to_be_removed.append(entry.entity_id)
+
+ # Remove unavailable entities
+ for entity_id in entities_to_be_removed:
+ entity_registry.async_remove(entity_id)
+
+ # Remove devices that don't belong to any entity
+ for device_id in devices_to_be_removed:
+ if len(async_entries_for_device(entity_registry, device_id)) == 0:
+ device_registry.async_remove_device(device_id)
diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml
index d8bf3e4d9949f7..9d85e76d8d3a53 100644
--- a/homeassistant/components/deconz/services.yaml
+++ b/homeassistant/components/deconz/services.yaml
@@ -23,3 +23,10 @@ device_refresh:
bridgeid:
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
example: "00212EFFFF012345"
+
+remove_orphaned_entries:
+ description: Clean up device and entity registry entries orphaned by deCONZ.
+ fields:
+ bridgeid:
+ description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
+ example: "00212EFFFF012345"
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index cf5acb5cff709f..ed854623ef8060 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -27,8 +27,9 @@
},
"abort": {
"already_configured": "Bridge is already configured",
- "already_in_progress": "Config flow for bridge is already in progress.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"no_bridges": "No deCONZ bridges discovered",
+ "no_hardware_available": "No radio hardware connected to deCONZ",
"not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
@@ -39,7 +40,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
- "allow_deconz_groups": "Allow deCONZ light groups"
+ "allow_deconz_groups": "Allow deCONZ light groups",
+ "allow_new_devices": "Allow automatic addition of new devices"
},
"description": "Configure visibility of deCONZ device types",
"title": "deCONZ options"
@@ -100,4 +102,4 @@
"side_6": "Side 6"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py
index d7b6b55fbb884b..02fa66fc35a336 100644
--- a/homeassistant/components/deconz/switch.py
+++ b/homeassistant/components/deconz/switch.py
@@ -1,5 +1,5 @@
"""Support for deCONZ switches."""
-from homeassistant.components.switch import SwitchEntity
+from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -8,16 +8,13 @@
from .gateway import get_gateway_from_config_entry
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ platforms."""
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up switches for deCONZ component.
- Switches are based same device class as lights in deCONZ.
+ Switches are based on the same device class as lights in deCONZ.
"""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ gateway.entities[DOMAIN] = set()
@callback
def async_add_switch(lights):
@@ -26,13 +23,19 @@ def async_add_switch(lights):
for light in lights:
- if light.type in POWER_PLUGS:
+ if (
+ light.type in POWER_PLUGS
+ and light.uniqueid not in gateway.entities[DOMAIN]
+ ):
entities.append(DeconzPowerPlug(light, gateway))
- elif light.type in SIRENS:
+ elif (
+ light.type in SIRENS and light.uniqueid not in gateway.entities[DOMAIN]
+ ):
entities.append(DeconzSiren(light, gateway))
- async_add_entities(entities, True)
+ if entities:
+ async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
@@ -46,6 +49,8 @@ def async_add_switch(lights):
class DeconzPowerPlug(DeconzDevice, SwitchEntity):
"""Representation of a deCONZ power plug."""
+ TYPE = DOMAIN
+
@property
def is_on(self):
"""Return true if switch is on."""
@@ -65,6 +70,8 @@ async def async_turn_off(self, **kwargs):
class DeconzSiren(DeconzDevice, SwitchEntity):
"""Representation of a deCONZ siren."""
+ TYPE = DOMAIN
+
@property
def is_on(self):
"""Return true if switch is on."""
diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json
index 1c8500b3cc1f64..77d948e8f4d49f 100644
--- a/homeassistant/components/deconz/translations/ca.json
+++ b/homeassistant/components/deconz/translations/ca.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades de configuraci\u00f3 per l'enlla\u00e7 ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ",
+ "no_hardware_available": "No hi ha cap maquinari r\u00e0dio connectat a deCONZ",
"not_deconz_bridge": "No \u00e9s un enlla\u00e7 deCONZ",
"one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ",
"updated_instance": "S'ha actualitzat la inst\u00e0ncia de deCONZ amb una nova adre\u00e7a"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Permet sensors deCONZ CLIP",
- "allow_deconz_groups": "Permet grups de llums deCONZ"
+ "allow_deconz_groups": "Permet grups de llums deCONZ",
+ "allow_new_devices": "Permet l'addici\u00f3 autom\u00e0tica de nous dispositius"
},
"description": "Configura la visibilitat dels tipus dels dispositius deCONZ",
"title": "Opcions de deCONZ"
diff --git a/homeassistant/components/deconz/translations/el.json b/homeassistant/components/deconz/translations/el.json
new file mode 100644
index 00000000000000..bb5489843b8608
--- /dev/null
+++ b/homeassistant/components/deconz/translations/el.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "no_hardware_available": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03c3\u03c4\u03bf deCONZ"
+ }
+ },
+ "options": {
+ "step": {
+ "deconz_devices": {
+ "data": {
+ "allow_new_devices": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bd\u03ad\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json
index fa329d2e1b3040..fc13160ce66c0b 100644
--- a/homeassistant/components/deconz/translations/en.json
+++ b/homeassistant/components/deconz/translations/en.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "Bridge is already configured",
- "already_in_progress": "Config flow for bridge is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"no_bridges": "No deCONZ bridges discovered",
+ "no_hardware_available": "No radio hardware connected to deCONZ",
"not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
- "allow_deconz_groups": "Allow deCONZ light groups"
+ "allow_deconz_groups": "Allow deCONZ light groups",
+ "allow_new_devices": "Allow automatic addition of new devices"
},
"description": "Configure visibility of deCONZ device types",
"title": "deCONZ options"
diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json
index 877623188bbac0..b00949b98c7a49 100644
--- a/homeassistant/components/deconz/translations/es.json
+++ b/homeassistant/components/deconz/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "La pasarela ya est\u00e1 configurada",
"already_in_progress": "El flujo de configuraci\u00f3n para la pasarela ya est\u00e1 en marcha.",
"no_bridges": "No se han descubierto pasarelas deCONZ",
+ "no_hardware_available": "No hay hardware de radio conectado a deCONZ",
"not_deconz_bridge": "No es una pasarela deCONZ",
"one_instance_only": "El componente solo admite una instancia de deCONZ",
"updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Permitir sensores deCONZ CLIP",
- "allow_deconz_groups": "Permitir grupos de luz deCONZ"
+ "allow_deconz_groups": "Permitir grupos de luz deCONZ",
+ "allow_new_devices": "Permitir a\u00f1adir autom\u00e1ticamente nuevos dispositivos"
},
"description": "Configurar la visibilidad de los tipos de dispositivos deCONZ",
"title": "Opciones deCONZ"
diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json
new file mode 100644
index 00000000000000..ff449b13e75b1f
--- /dev/null
+++ b/homeassistant/components/deconz/translations/et.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "no_hardware_available": "DeCONZi raadio riistvara puudub"
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "M\u00f5lemad nupud",
+ "bottom_buttons": "Alumised nupud",
+ "button_1": "Esimene nupp",
+ "button_2": "Teine nupp",
+ "button_3": "Kolmas nupp",
+ "button_4": "Neljas nupp",
+ "close": "Sulge",
+ "dim_down": "H\u00e4marda",
+ "dim_up": "Tee heledamaks",
+ "left": "Vasakpoolne",
+ "open": "Ava",
+ "right": "Parempoolne",
+ "side_1": "1. k\u00fclg",
+ "side_2": "2. k\u00fclg",
+ "side_3": "3. k\u00fclg",
+ "side_4": "4. k\u00fclg",
+ "side_5": "5. k\u00fclg",
+ "side_6": "6. k\u00fclg",
+ "top_buttons": "\u00dclemised nupud",
+ "turn_off": "L\u00fclita v\u00e4lja",
+ "turn_on": "L\u00fclita sisse"
+ },
+ "trigger_type": {
+ "remote_awakened": "Seade \u00e4rkas",
+ "remote_button_rotation_stopped": "Nupu \" {subtype} \" p\u00f6\u00f6ramine peatus"
+ }
+ },
+ "options": {
+ "step": {
+ "deconz_devices": {
+ "data": {
+ "allow_new_devices": "Luba uute seadmete automaatne lisamine"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json
index 4fad292f27443f..2704951db5cd95 100644
--- a/homeassistant/components/deconz/translations/fr.json
+++ b/homeassistant/components/deconz/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
"already_in_progress": "Le flux de configuration pour le pont est d\u00e9j\u00e0 en cours.",
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
+ "no_hardware_available": "Aucun mat\u00e9riel radio connect\u00e9 \u00e0 deCONZ",
"not_deconz_bridge": "Pas un pont deCONZ",
"one_instance_only": "Le composant prend uniquement en charge une instance deCONZ",
"updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te"
@@ -26,6 +27,11 @@
"host": "Nom d'h\u00f4te ou adresse IP",
"port": "Port"
}
+ },
+ "user": {
+ "data": {
+ "host": "S\u00e9lectionnez la passerelle deCONZ d\u00e9couverte"
+ }
}
}
},
@@ -88,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP",
- "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ"
+ "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ",
+ "allow_new_devices": "Autoriser l'ajout automatique de nouveaux appareils"
},
"description": "Configurer la visibilit\u00e9 des appareils de type deCONZ",
"title": "Options deCONZ"
diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json
index 26a1a14e32a5c2..2029e11613fe79 100644
--- a/homeassistant/components/deconz/translations/it.json
+++ b/homeassistant/components/deconz/translations/it.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "Il Bridge \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"no_bridges": "Nessun bridge deCONZ rilevato",
+ "no_hardware_available": "Nessun hardware radio collegato a deCONZ",
"not_deconz_bridge": "Non \u00e8 un bridge deCONZ",
"one_instance_only": "Il componente supporto solo un'istanza di deCONZ",
"updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Consentire sensori CLIP deCONZ",
- "allow_deconz_groups": "Consentire gruppi luce deCONZ"
+ "allow_deconz_groups": "Consentire gruppi luce deCONZ",
+ "allow_new_devices": "Consentire l'aggiunta automatica di nuovi dispositivi"
},
"description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ",
"title": "Opzioni deCONZ"
diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json
index 6e80c2393aca73..d2c36eb30bd4b1 100644
--- a/homeassistant/components/deconz/translations/lb.json
+++ b/homeassistant/components/deconz/translations/lb.json
@@ -4,6 +4,7 @@
"already_configured": "Bridge ass schon konfigur\u00e9iert",
"already_in_progress": "Konfiguratioun fir d\u00ebsen Apparat ass schonn am gaang.",
"no_bridges": "Keng dECONZ bridges fonnt",
+ "no_hardware_available": "Keng Radio Hardware verbonne mat deCONZ",
"not_deconz_bridge": "Keng deCONZ Bridge",
"one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz",
"updated_instance": "deCONZ Instanz gouf mat der neier Adress vum Apparat ge\u00e4nnert"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "deCONZ Clip Sensoren erlaben",
- "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben"
+ "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben",
+ "allow_new_devices": "Erlaabt automatesch dob\u00e4isetze vu neien Apparater"
},
"description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren",
"title": "deCONZ Optiounen"
diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json
index 231901c4cd404b..932b5d82da0167 100644
--- a/homeassistant/components/deconz/translations/no.json
+++ b/homeassistant/components/deconz/translations/no.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "Broen er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for bro p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"no_bridges": "Ingen deCONZ broer oppdaget",
+ "no_hardware_available": "Ingen radiomaskinvare koblet til deCONZ",
"not_deconz_bridge": "Ikke en deCONZ bro",
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst",
"updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "Tillat deCONZ CLIP-sensorer",
- "allow_deconz_groups": "Tillat deCONZ lys grupper"
+ "allow_deconz_groups": "Tillat deCONZ lys grupper",
+ "allow_new_devices": "Tillat automatisk tilsetning av nye enheter"
},
"description": "Konfigurere synlighet av deCONZ enhetstyper",
"title": "deCONZ alternativer"
diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json
index 27dbc0535cebd1..2fa9549585a251 100644
--- a/homeassistant/components/deconz/translations/pl.json
+++ b/homeassistant/components/deconz/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Mostek jest ju\u017c skonfigurowany.",
+ "already_configured": "Mostek jest ju\u017c skonfigurowany",
"already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.",
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
"not_deconz_bridge": "To nie jest mostek deCONZ",
diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json
index 32bf2001f44b23..794c2e52bd2243 100644
--- a/homeassistant/components/deconz/translations/ru.json
+++ b/homeassistant/components/deconz/translations/ru.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
+ "no_hardware_available": "\u041a deCONZ \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0440\u0430\u0434\u0438\u043e\u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435.",
"not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.",
"updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP",
- "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ"
+ "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ",
+ "allow_new_devices": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 deCONZ"
diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json
index 2b941f5ae64b8c..088ee458abec34 100644
--- a/homeassistant/components/deconz/translations/zh-Hant.json
+++ b/homeassistant/components/deconz/translations/zh-Hant.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe",
+ "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u8a2d\u5099\u9023\u7dda",
"not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099",
"one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6",
"updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u8a2d\u5099"
@@ -93,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668",
- "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44"
+ "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44",
+ "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u8a2d\u5099"
},
"description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b",
"title": "deCONZ \u9078\u9805"
diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py
index 4335c99076a84d..2fc436eda20e1e 100644
--- a/homeassistant/components/decora/light.py
+++ b/homeassistant/components/decora/light.py
@@ -125,11 +125,6 @@ def supported_features(self):
"""Flag supported features."""
return SUPPORT_DECORA_LED
- @property
- def should_poll(self):
- """We can read the device state, so poll."""
- return True
-
@property
def assumed_state(self):
"""We can read the actual state."""
diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py
index 04d8e72f9a8005..c4186eae5056b6 100644
--- a/homeassistant/components/demo/binary_sensor.py
+++ b/homeassistant/components/demo/binary_sensor.py
@@ -1,5 +1,9 @@
"""Demo platform that has two fake binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ BinarySensorEntity,
+)
from . import DOMAIN
@@ -8,8 +12,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""Set up the Demo binary sensor platform."""
async_add_entities(
[
- DemoBinarySensor("binary_1", "Basement Floor Wet", False, "moisture"),
- DemoBinarySensor("binary_2", "Movement Backyard", True, "motion"),
+ DemoBinarySensor(
+ "binary_1", "Basement Floor Wet", False, DEVICE_CLASS_MOISTURE
+ ),
+ DemoBinarySensor(
+ "binary_2", "Movement Backyard", True, DEVICE_CLASS_MOTION
+ ),
]
)
diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py
index ce211a47594df5..2059f258972f78 100644
--- a/homeassistant/components/demo/camera.py
+++ b/homeassistant/components/demo/camera.py
@@ -40,14 +40,6 @@ def name(self):
"""Return the name of this camera."""
return self._name
- @property
- def should_poll(self):
- """Demo camera doesn't need poll.
-
- Need explicitly call async_write_ha_state() after state changed.
- """
- return False
-
@property
def supported_features(self):
"""Camera support turn on/off features."""
diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json
index 5003b9da5684bb..48da80fd62999b 100644
--- a/homeassistant/components/demo/translations/no.json
+++ b/homeassistant/components/demo/translations/no.json
@@ -5,7 +5,7 @@
"data": {
"bool": "Valgfri boolean",
"constant": "Konstant",
- "int": "Numerisk inndata"
+ "int": "Numerisk innputt"
}
},
"options_2": {
diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py
index 9f451ab3025712..ed90c2ddcb00db 100644
--- a/homeassistant/components/denon/media_player.py
+++ b/homeassistant/components/denon/media_player.py
@@ -111,10 +111,18 @@ def _setup_sources(self, telnet):
if nsfrn:
self._name = nsfrn
- # SSFUN - Configured sources with names
+ # SSFUN - Configured sources with (optional) names
self._source_list = {}
for line in self.telnet_request(telnet, "SSFUN ?", all_lines=True):
- source, configured_name = line[len("SSFUN") :].split(" ", 1)
+ ssfun = line[len("SSFUN") :].split(" ", 1)
+
+ source = ssfun[0]
+ if len(ssfun) == 2 and ssfun[1]:
+ configured_name = ssfun[1]
+ else:
+ # No name configured, reusing the source name
+ configured_name = source
+
self._source_list[configured_name] = source
# SSSOD - Deleted sources
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index afc5a7ef72f825..86bee686764f88 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -3,7 +3,7 @@
"name": "Denon AVR Network Receivers",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/denonavr",
- "requirements": ["denonavr==0.9.4", "getmac==0.8.2"],
+ "requirements": ["denonavr==0.9.5", "getmac==0.8.2"],
"codeowners": ["@scarface-4711", "@starkillerOG"],
"ssdp": [
{
diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py
index c28b1a4cab5f57..b5990dede21310 100644
--- a/homeassistant/components/denonavr/media_player.py
+++ b/homeassistant/components/denonavr/media_player.py
@@ -308,14 +308,13 @@ def media_episode(self):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
- attributes = {}
if (
self._sound_mode_raw is not None
and self._sound_mode_support
and self._power == "ON"
):
- attributes[ATTR_SOUND_MODE_RAW] = self._sound_mode_raw
- return attributes
+ return {ATTR_SOUND_MODE_RAW: self._sound_mode_raw}
+ return {}
def media_play_pause(self):
"""Play or pause the media player."""
diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json
index 25648d974abcf7..6ac96f2c17c7f5 100644
--- a/homeassistant/components/denonavr/strings.json
+++ b/homeassistant/components/denonavr/strings.json
@@ -6,7 +6,7 @@
"title": "Denon AVR Network Receivers",
"description": "Connect to your receiver, if the IP address is not set, auto-discovery is used",
"data": {
- "host": "IP address"
+ "host": "[%key:common::config_flow::data::ip%]"
}
},
"confirm": {
@@ -26,7 +26,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "Config flow for this Denon AVR is already in progress",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"connection_error": "Failed to connect, please try again, disconnecting mains power and ethernet cables and reconnecting them may help",
"not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match",
"not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete"
diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json
index 34e73124056b40..bf67ba257ca16e 100644
--- a/homeassistant/components/denonavr/translations/ca.json
+++ b/homeassistant/components/denonavr/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades de configuraci\u00f3 per aquest Denon AVR ja est\u00e0 en curs",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"connection_error": "No s'ha pogut connectar, torna-ho a provar. \u00c9s possible que s'arregli si desconnectes i tornes a connectar els cables d'Ethernet i d'alimentaci\u00f3.",
"not_denonavr_manufacturer": "No \u00e9s un receptor de xarxa Denon AVR, no coincideix el fabricant descobert",
"not_denonavr_missing": "No \u00e9s un receptor de xarxa Denon AVR, informaci\u00f3 de descobriment no completa"
diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json
new file mode 100644
index 00000000000000..4fd05d6c578b35
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/de.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "IP-Adresse"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json
index 91ec0d62a45327..4d2b16352e2ab2 100644
--- a/homeassistant/components/denonavr/translations/en.json
+++ b/homeassistant/components/denonavr/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "Config flow for this Denon AVR is already in progress",
+ "already_in_progress": "Configuration flow is already in progress",
"connection_error": "Failed to connect, please try again, disconnecting mains power and ethernet cables and reconnecting them may help",
"not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match",
"not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete"
@@ -25,7 +25,7 @@
},
"user": {
"data": {
- "host": "IP address"
+ "host": "IP Address"
},
"description": "Connect to your receiver, if the IP address is not set, auto-discovery is used",
"title": "Denon AVR Network Receivers"
diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json
new file mode 100644
index 00000000000000..77c31e46107a35
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "connection_error": "\u00dchenduse loomine nurjus. Vooluv\u00f5rgust ja LAN v\u00f5rgust eemaldamine ja taas\u00fchendamine v\u00f5ib aidata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json
index 4b6359cfce626b..9fc4e3cf90f3c8 100644
--- a/homeassistant/components/denonavr/translations/it.json
+++ b/homeassistant/components/denonavr/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per questo Denon AVR \u00e8 gi\u00e0 in corso",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"connection_error": "Impossibile connettersi, si prega di riprovare, pu\u00f2 essere utile scollegare i cavi di alimentazione ed i cavi Ethernet e ricollegarli",
"not_denonavr_manufacturer": "Non \u00e8 un ricevitore di rete Denon AVR, il produttore rilevato non corrisponde",
"not_denonavr_missing": "Non \u00e8 un ricevitore di rete Denon AVR, le informazioni di rilevamento non sono complete"
diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json
index e156101c378da5..2ae9a550c3d592 100644
--- a/homeassistant/components/denonavr/translations/no.json
+++ b/homeassistant/components/denonavr/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for denne Denon AVR p\u00e5g\u00e5r allerede",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"connection_error": "Kunne ikke koble til, vennligst pr\u00f8v igjen. Koble fra str\u00f8m- og nettverkskablene og koble dem til igjen kan hjelpe",
"not_denonavr_manufacturer": "Ikke en Denon AVR Network Receiver, oppdaget manafucturer stemte ikke overens",
"not_denonavr_missing": "Ikke en Denon AVR Network Receiver, oppdagelsesinformasjon ikke fullf\u00f8rt"
diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json
index f025f6e2dd4d79..eb396842bf0817 100644
--- a/homeassistant/components/denonavr/translations/pl.json
+++ b/homeassistant/components/denonavr/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem, spr\u00f3buj ponownie, od\u0142\u0105czenie zasilania sieciowego i kabla Ethernet i ponowne pod\u0142\u0105czenie mo\u017ce pom\u00f3c",
"not_denonavr_manufacturer": "Nie jest to urz\u0105dzenie AVR firmy Denon, producent wykrytego urz\u0105dzenia nie pasuje.",
diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json
index 79e8c213e5f472..6dddf729fe358e 100644
--- a/homeassistant/components/denonavr/translations/ru.json
+++ b/homeassistant/components/denonavr/translations/ru.json
@@ -2,7 +2,7 @@
"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": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437. \u0415\u0441\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u043a\u0430\u0431\u0435\u043b\u044c.",
"not_denonavr_manufacturer": "\u042d\u0442\u043e \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442.",
"not_denonavr_missing": "\u041d\u0435\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430."
diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json
index 6127d1703054c5..5dfe1df53b24ca 100644
--- a/homeassistant/components/denonavr/translations/zh-Hant.json
+++ b/homeassistant/components/denonavr/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "Denon AVR \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"connection_error": "\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_missing": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u63a2\u7d22\u8cc7\u8a0a\u4e0d\u5b8c\u6574"
diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py
index c1369dd0f5b0b2..7c2cc444a5a25f 100644
--- a/homeassistant/components/derivative/sensor.py
+++ b/homeassistant/components/derivative/sensor.py
@@ -213,8 +213,7 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- state_attr = {ATTR_SOURCE_ID: self._sensor_source_id}
- return state_attr
+ return {ATTR_SOURCE_ID: self._sensor_source_id}
@property
def icon(self):
diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py
new file mode 100644
index 00000000000000..07ec2cfe985ed7
--- /dev/null
+++ b/homeassistant/components/device_tracker/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)
diff --git a/homeassistant/components/device_tracker/translations/et.json b/homeassistant/components/device_tracker/translations/et.json
index 340c03665ff230..c4f2b6f277d9da 100644
--- a/homeassistant/components/device_tracker/translations/et.json
+++ b/homeassistant/components/device_tracker/translations/et.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} on kodus",
+ "is_not_home": "{entity_name} on eemal"
+ }
+ },
"state": {
"_": {
"home": "Kodus",
diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py
index c955bf770966da..ba180fd6ea49a0 100644
--- a/homeassistant/components/devolo_home_control/__init__.py
+++ b/homeassistant/components/devolo_home_control/__init__.py
@@ -4,7 +4,7 @@
from devolo_home_control_api.homecontrol import HomeControl
from devolo_home_control_api.mydevolo import Mydevolo
-from homeassistant.components import switch as ha_switch
+from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.exceptions import ConfigEntryNotReady
@@ -12,8 +12,6 @@
from .const import CONF_HOMECONTROL, CONF_MYDEVOLO, DOMAIN, PLATFORMS
-SUPPORTED_PLATFORMS = [ha_switch.DOMAIN]
-
async def async_setup(hass, config):
"""Get all devices and add them to hass."""
@@ -32,7 +30,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
mydevolo.user = conf[CONF_USERNAME]
mydevolo.password = conf[CONF_PASSWORD]
mydevolo.url = conf[CONF_MYDEVOLO]
- mydevolo.mprm = conf[CONF_HOMECONTROL]
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
@@ -44,11 +41,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
gateway_id = gateway_ids[0]
- mprm_url = mydevolo.mprm
try:
+ zeroconf_instance = await zeroconf.async_get_instance(hass)
hass.data[DOMAIN]["homecontrol"] = await hass.async_add_executor_job(
- partial(HomeControl, gateway_id=gateway_id, url=mprm_url)
+ partial(
+ HomeControl,
+ gateway_id=gateway_id,
+ zeroconf_instance=zeroconf_instance,
+ url=conf[CONF_HOMECONTROL],
+ )
)
except ConnectionError as err:
raise ConfigEntryNotReady from err
diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py
index e05350dbbacd67..f884ce0219d944 100644
--- a/homeassistant/components/devolo_home_control/binary_sensor.py
+++ b/homeassistant/components/devolo_home_control/binary_sensor.py
@@ -67,50 +67,35 @@ def __init__(self, homecontrol, device_instance, element_uid):
element_uid
)
+ super().__init__(
+ homecontrol=homecontrol,
+ device_instance=device_instance,
+ element_uid=element_uid,
+ )
+
self._device_class = DEVICE_CLASS_MAPPING.get(
self._binary_sensor_property.sub_type
or self._binary_sensor_property.sensor_type
)
- name = device_instance.item_name
if self._device_class is None:
if device_instance.binary_sensor_property.get(element_uid).sub_type != "":
- name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}"
+ self._name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}"
else:
- name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}"
+ self._name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}"
- super().__init__(
- homecontrol=homecontrol,
- device_instance=device_instance,
- element_uid=element_uid,
- name=name,
- sync=self._sync,
- )
-
- self._state = self._binary_sensor_property.state
-
- self._subscriber = None
+ self._value = self._binary_sensor_property.state
@property
def is_on(self):
"""Return the state."""
- return self._state
+ return self._value
@property
def device_class(self):
"""Return device class."""
return self._device_class
- def _sync(self, message=None):
- """Update the binary sensor state."""
- if message[0].startswith("devolo.BinarySensor"):
- self._state = self._device_instance.binary_sensor_property[message[0]].state
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
- else:
- _LOGGER.debug("No valid message received: %s", message)
- self.schedule_update_ha_state()
-
class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
"""Representation of a remote control within devolo Home Control."""
@@ -120,26 +105,22 @@ def __init__(self, homecontrol, device_instance, element_uid, key):
self._remote_control_property = device_instance.remote_control_property.get(
element_uid
)
+
super().__init__(
homecontrol=homecontrol,
device_instance=device_instance,
element_uid=f"{element_uid}_{key}",
- name=device_instance.item_name,
- sync=self._sync,
)
self._key = key
-
self._state = False
- self._subscriber = None
-
@property
def is_on(self):
"""Return the state."""
return self._state
- def _sync(self, message=None):
+ def _sync(self, message):
"""Update the binary sensor state."""
if (
message[0] == self._remote_control_property.element_uid
@@ -150,8 +131,6 @@ def _sync(self, message=None):
message[0] == self._remote_control_property.element_uid and message[1] == 0
):
self._state = False
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
else:
- _LOGGER.debug("No valid message received: %s", message)
+ self._generic_message(message)
self.schedule_update_ha_state()
diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py
index d44a0c981f1175..297e431e63a765 100644
--- a/homeassistant/components/devolo_home_control/climate.py
+++ b/homeassistant/components/devolo_home_control/climate.py
@@ -14,7 +14,7 @@
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN
-from .devolo_device import DevoloDeviceEntity
+from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ async def async_setup_entry(
for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices:
for multi_level_switch in device.multi_level_switch_property:
- if device.deviceModelUID in [
+ if device.device_model_uid in [
"devolo.model.Thermostat:Valve",
"devolo.model.Room:Thermostat",
]:
@@ -42,29 +42,13 @@ async def async_setup_entry(
async_add_entities(entities, False)
-class DevoloClimateDeviceEntity(DevoloDeviceEntity, ClimateEntity):
+class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntity):
"""Representation of a climate/thermostat device within devolo Home Control."""
- def __init__(self, homecontrol, device_instance, element_uid):
- """Initialize a devolo climate/thermostat device."""
- super().__init__(
- homecontrol=homecontrol,
- device_instance=device_instance,
- element_uid=element_uid,
- name=device_instance.item_name,
- sync=self._sync,
- )
-
- self._multi_level_switch_property = (
- device_instance.multi_level_switch_property.get(element_uid)
- )
-
- self._temperature = self._multi_level_switch_property.value
-
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
- return self._temperature
+ return self._value
@property
def hvac_mode(self) -> str:
@@ -104,13 +88,3 @@ def temperature_unit(self) -> str:
def set_temperature(self, **kwargs):
"""Set new target temperature."""
self._multi_level_switch_property.set(kwargs[ATTR_TEMPERATURE])
-
- def _sync(self, message=None):
- """Update the climate entity triggered by web socket connection."""
- if message[0] == self._unique_id:
- self._temperature = message[1]
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
- else:
- _LOGGER.debug("Not valid message received: %s", message)
- self.schedule_update_ha_state()
diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py
index 93b2cfc11e5f70..cde55ebb4bfd41 100644
--- a/homeassistant/components/devolo_home_control/config_flow.py
+++ b/homeassistant/components/devolo_home_control/config_flow.py
@@ -38,8 +38,8 @@ async def async_step_user(self, user_input=None):
self.data_schema = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
- vol.Required(CONF_MYDEVOLO): str,
- vol.Required(CONF_HOMECONTROL): str,
+ vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str,
+ vol.Required(CONF_HOMECONTROL, default=DEFAULT_MPRM): str,
}
if user_input is None:
return self._show_form(user_input)
@@ -53,15 +53,15 @@ async def async_step_user(self, user_input=None):
mydevolo.password = password
if self.show_advanced_options:
mydevolo.url = user_input[CONF_MYDEVOLO]
- mydevolo.mprm = user_input[CONF_HOMECONTROL]
+ mprm = user_input[CONF_HOMECONTROL]
else:
mydevolo.url = DEFAULT_MYDEVOLO
- mydevolo.mprm = DEFAULT_MPRM
+ mprm = DEFAULT_MPRM
credentials_valid = await self.hass.async_add_executor_job(
mydevolo.credentials_valid
)
if not credentials_valid:
- return self._show_form({"base": "invalid_credentials"})
+ return self._show_form({"base": "invalid_auth"})
_LOGGER.debug("Credentials valid")
gateway_ids = await self.hass.async_add_executor_job(mydevolo.get_gateway_ids)
await self.async_set_unique_id(gateway_ids[0])
@@ -73,7 +73,7 @@ async def async_step_user(self, user_input=None):
CONF_PASSWORD: password,
CONF_USERNAME: user,
CONF_MYDEVOLO: mydevolo.url,
- CONF_HOMECONTROL: mydevolo.mprm,
+ CONF_HOMECONTROL: mprm,
},
)
diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py
index b93713cc7005b4..21e555e3122891 100644
--- a/homeassistant/components/devolo_home_control/cover.py
+++ b/homeassistant/components/devolo_home_control/cover.py
@@ -12,7 +12,7 @@
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN
-from .devolo_device import DevoloDeviceEntity
+from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
_LOGGER = logging.getLogger(__name__)
@@ -37,29 +37,13 @@ async def async_setup_entry(
async_add_entities(entities, False)
-class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity):
+class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity):
"""Representation of a cover device within devolo Home Control."""
- def __init__(self, homecontrol, device_instance, element_uid):
- """Initialize a devolo blinds device."""
- super().__init__(
- homecontrol=homecontrol,
- device_instance=device_instance,
- element_uid=element_uid,
- name=device_instance.item_name,
- sync=self._sync,
- )
-
- self._multi_level_switch_property = (
- device_instance.multi_level_switch_property.get(element_uid)
- )
-
- self._position = self._multi_level_switch_property.value
-
@property
def current_cover_position(self):
"""Return the current position. 0 is closed. 100 is open."""
- return self._position
+ return self._value
@property
def device_class(self):
@@ -69,7 +53,7 @@ def device_class(self):
@property
def is_closed(self):
"""Return if the blind is closed or not."""
- return not bool(self._position)
+ return not bool(self._value)
@property
def supported_features(self):
@@ -87,13 +71,3 @@ def close_cover(self, **kwargs):
def set_cover_position(self, **kwargs):
"""Set the blind to the given position."""
self._multi_level_switch_property.set(kwargs["position"])
-
- def _sync(self, message=None):
- """Update the binary sensor state."""
- if message[0] == self._unique_id:
- self._position = message[1]
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
- else:
- _LOGGER.debug("Not valid message received: %s", message)
- self.schedule_update_ha_state()
diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py
index 06ddf2175f733b..48f9b59af4a2a4 100644
--- a/homeassistant/components/devolo_home_control/devolo_device.py
+++ b/homeassistant/components/devolo_home_control/devolo_device.py
@@ -10,14 +10,17 @@
class DevoloDeviceEntity(Entity):
- """Representation of a sensor within devolo Home Control."""
+ """Abstract representation of a device within devolo Home Control."""
- def __init__(self, homecontrol, device_instance, element_uid, name, sync):
+ def __init__(self, homecontrol, device_instance, element_uid):
"""Initialize a devolo device entity."""
self._device_instance = device_instance
- self._name = name
self._unique_id = element_uid
self._homecontrol = homecontrol
+ self._name = device_instance.settings_property["general_device_settings"].name
+ self._device_class = None
+ self._value = None
+ self._unit = None
# This is not doing I/O. It fetches an internal state of the API
self._available = device_instance.is_online()
@@ -27,13 +30,11 @@ def __init__(self, homecontrol, device_instance, element_uid, name, sync):
self._model = device_instance.name
self.subscriber = None
- self.sync_callback = sync
+ self.sync_callback = self._sync
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
- self.subscriber = Subscriber(
- self._device_instance.item_name, callback=self.sync_callback
- )
+ self.subscriber = Subscriber(self._name, callback=self.sync_callback)
self._homecontrol.publisher.register(
self._device_instance.uid, self.subscriber, self.sync_callback
)
@@ -54,7 +55,7 @@ def device_info(self):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self._device_instance.uid)},
- "name": self._device_instance.item_name,
+ "name": self._name,
"manufacturer": self._brand,
"model": self._model,
}
@@ -73,3 +74,21 @@ def name(self):
def available(self) -> bool:
"""Return the online state."""
return self._available
+
+ def _sync(self, message):
+ """Update the state."""
+ if message[0] == self._unique_id:
+ self._value = message[1]
+ else:
+ self._generic_message(message)
+ self.schedule_update_ha_state()
+
+ def _generic_message(self, message):
+ """Handle generic messages."""
+ if len(message) == 3 and message[2] == "battery_level":
+ self._value = message[1]
+ elif len(message) == 3 and message[2] == "status":
+ # Maybe the API wants to tell us, that the device went on- or offline.
+ self._available = self._device_instance.is_online()
+ else:
+ _LOGGER.debug("No valid message received: %s", message)
diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
index 70629854deac75..8056192340c934 100644
--- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
+++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
@@ -15,21 +15,9 @@ def __init__(self, homecontrol, device_instance, element_uid):
homecontrol=homecontrol,
device_instance=device_instance,
element_uid=element_uid,
- name=device_instance.item_name,
- sync=self._sync,
)
self._multi_level_switch_property = device_instance.multi_level_switch_property[
element_uid
]
self._value = self._multi_level_switch_property.value
-
- def _sync(self, message):
- """Update the multi level switch state."""
- if message[0] == self._multi_level_switch_property.element_uid:
- self._value = message[1]
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
- else:
- _LOGGER.debug("No valid message received: %s", message)
- self.schedule_update_ha_state()
diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json
index bdb3a80fff69e6..359fc76b4f81cc 100644
--- a/homeassistant/components/devolo_home_control/manifest.json
+++ b/homeassistant/components/devolo_home_control/manifest.json
@@ -2,7 +2,8 @@
"domain": "devolo_home_control",
"name": "devolo Home Control",
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
- "requirements": ["devolo-home-control-api==0.13.0"],
+ "requirements": ["devolo-home-control-api==0.15.0"],
+ "after_dependencies": ["zeroconf"],
"config_flow": true,
"codeowners": ["@2Fake", "@Shutgun"],
"quality_scale": "silver"
diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py
index 4bb2536dcc2997..d32a2887842830 100644
--- a/homeassistant/components/devolo_home_control/sensor.py
+++ b/homeassistant/components/devolo_home_control/sensor.py
@@ -2,12 +2,14 @@
import logging
from homeassistant.components.sensor import (
+ DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import PERCENTAGE
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN
@@ -16,6 +18,7 @@
_LOGGER = logging.getLogger(__name__)
DEVICE_CLASS_MAPPING = {
+ "battery": DEVICE_CLASS_BATTERY,
"temperature": DEVICE_CLASS_TEMPERATURE,
"light": DEVICE_CLASS_ILLUMINANCE,
"humidity": DEVICE_CLASS_HUMIDITY,
@@ -33,7 +36,7 @@ async def async_setup_entry(
for device in hass.data[DOMAIN]["homecontrol"].multi_level_sensor_devices:
for multi_level_sensor in device.multi_level_sensor_property:
entities.append(
- DevoloMultiLevelDeviceEntity(
+ DevoloGenericMultiLevelDeviceEntity(
homecontrol=hass.data[DOMAIN]["homecontrol"],
device_instance=device,
element_uid=multi_level_sensor,
@@ -51,75 +54,83 @@ async def async_setup_entry(
consumption=consumption_type,
)
)
+ if hasattr(device, "battery_level"):
+ entities.append(
+ DevoloBatteryEntity(
+ homecontrol=hass.data[DOMAIN]["homecontrol"],
+ device_instance=device,
+ element_uid=f"devolo.BatterySensor:{device.uid}",
+ )
+ )
async_add_entities(entities, False)
class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity):
- """Representation of a multi level sensor within devolo Home Control."""
+ """Abstract representation of a multi level sensor within devolo Home Control."""
+
+ @property
+ def device_class(self) -> str:
+ """Return device class."""
+ return self._device_class
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._value
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity."""
+ return self._unit
+
+
+class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity):
+ """Representation of a generic multi level sensor within devolo Home Control."""
def __init__(
self,
homecontrol,
device_instance,
element_uid,
- multi_level_sensor_property=None,
- sync=None,
):
"""Initialize a devolo multi level sensor."""
- if multi_level_sensor_property is None:
- self._multi_level_sensor_property = (
- device_instance.multi_level_sensor_property[element_uid]
- )
- else:
- self._multi_level_sensor_property = multi_level_sensor_property
+ self._multi_level_sensor_property = device_instance.multi_level_sensor_property[
+ element_uid
+ ]
- self._state = self._multi_level_sensor_property.value
+ super().__init__(
+ homecontrol=homecontrol,
+ device_instance=device_instance,
+ element_uid=element_uid,
+ )
self._device_class = DEVICE_CLASS_MAPPING.get(
self._multi_level_sensor_property.sensor_type
)
- name = device_instance.item_name
+ self._value = self._multi_level_sensor_property.value
+ self._unit = self._multi_level_sensor_property.unit
if self._device_class is None:
- name += f" {self._multi_level_sensor_property.sensor_type}"
+ self._name += f" {self._multi_level_sensor_property.sensor_type}"
- self._unit = self._multi_level_sensor_property.unit
+
+class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity):
+ """Representation of a battery entity within devolo Home Control."""
+
+ def __init__(self, homecontrol, device_instance, element_uid):
+ """Initialize a battery sensor."""
super().__init__(
homecontrol=homecontrol,
device_instance=device_instance,
element_uid=element_uid,
- name=name,
- sync=self._sync if sync is None else sync,
)
- @property
- def device_class(self) -> str:
- """Return device class."""
- return self._device_class
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity."""
- return self._unit
+ self._device_class = DEVICE_CLASS_MAPPING.get("battery")
- def _sync(self, message=None):
- """Update the multi level sensor state."""
- if message[0] == self._multi_level_sensor_property.element_uid:
- self._state = self._device_instance.multi_level_sensor_property[
- message[0]
- ].value
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
- else:
- _LOGGER.debug("No valid message received: %s", message)
- self.schedule_update_ha_state()
+ self._value = device_instance.battery_level
+ self._unit = PERCENTAGE
class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
@@ -127,39 +138,37 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
def __init__(self, homecontrol, device_instance, element_uid, consumption):
"""Initialize a devolo consumption sensor."""
- self._device_instance = device_instance
- self.value = getattr(
+ super().__init__(
+ homecontrol=homecontrol,
+ device_instance=device_instance,
+ element_uid=element_uid,
+ )
+
+ self._sensor_type = consumption
+ self._device_class = DEVICE_CLASS_MAPPING.get(consumption)
+
+ self._value = getattr(
device_instance.consumption_property[element_uid], consumption
)
- self.sensor_type = consumption
- self.unit = getattr(
+ self._unit = getattr(
device_instance.consumption_property[element_uid], f"{consumption}_unit"
)
- self.element_uid = element_uid
- super().__init__(
- homecontrol,
- device_instance,
- element_uid,
- multi_level_sensor_property=self,
- sync=self._sync,
- )
+ self._name += f" {consumption}"
@property
def unique_id(self):
"""Return the unique ID of the entity."""
- return f"{self._unique_id}_{self.sensor_type}"
+ return f"{self._unique_id}_{self._sensor_type}"
- def _sync(self, message=None):
+ def _sync(self, message):
"""Update the consumption sensor state."""
- if message[0] == self.element_uid:
- self._state = getattr(
- self._device_instance.consumption_property[self.element_uid],
- self.sensor_type,
+ if message[0] == self._unique_id:
+ self._value = getattr(
+ self._device_instance.consumption_property[self._unique_id],
+ self._sensor_type,
)
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
else:
- _LOGGER.debug("No valid message received: %s", message)
+ self._generic_message(message)
self.schedule_update_ha_state()
diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json
index 620ca4218546bf..8c73831a229bfb 100644
--- a/homeassistant/components/devolo_home_control/strings.json
+++ b/homeassistant/components/devolo_home_control/strings.json
@@ -2,21 +2,21 @@
"title": "devolo Home Control",
"config": {
"abort": {
- "already_configured": "This Home Control Central Unit is already in use."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
- "invalid_credentials": "Incorrect user name and/or password."
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"step": {
"user": {
"data": {
- "username": "E-Mail-Address / devolo ID",
+ "username": "[%key:common::config_flow::data::email%] / devolo ID",
"password": "[%key:common::config_flow::data::password%]",
- "mydevolo_url": "mydevolo URL",
- "home_control_url": "Home Control URL"
+ "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]",
+ "home_control_url": "Home Control [%key:common::config_flow::data::url%]"
},
"title": "devolo Home Control"
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py
index 9a7812af7bd80f..bc62dbfe2b9eb6 100644
--- a/homeassistant/components/devolo_home_control/switch.py
+++ b/homeassistant/components/devolo_home_control/switch.py
@@ -6,6 +6,7 @@
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN
+from .devolo_device import DevoloDeviceEntity
_LOGGER = logging.getLogger(__name__)
@@ -32,26 +33,16 @@ async def async_setup_entry(
async_add_entities(entities)
-class DevoloSwitch(SwitchEntity):
+class DevoloSwitch(DevoloDeviceEntity, SwitchEntity):
"""Representation of a switch."""
def __init__(self, homecontrol, device_instance, element_uid):
"""Initialize an devolo Switch."""
- self._device_instance = device_instance
-
- # Create the unique ID
- self._unique_id = element_uid
-
- self._homecontrol = homecontrol
- self._name = self._device_instance.item_name
-
- # This is not doing I/O. It fetches an internal state of the API
- self._available = self._device_instance.is_online()
-
- # Get the brand and model information
- self._brand = self._device_instance.brand
- self._model = self._device_instance.name
-
+ super().__init__(
+ homecontrol=homecontrol,
+ device_instance=device_instance,
+ element_uid=element_uid,
+ )
self._binary_switch_property = self._device_instance.binary_switch_property.get(
self._unique_id
)
@@ -64,47 +55,6 @@ def __init__(self, homecontrol, device_instance, element_uid):
else:
self._consumption = None
- self.subscriber = None
-
- async def async_added_to_hass(self):
- """Call when entity is added to hass."""
- self.subscriber = Subscriber(
- self._device_instance.item_name, callback=self.sync
- )
- self._homecontrol.publisher.register(
- self._device_instance.uid, self.subscriber, self.sync
- )
-
- @property
- def unique_id(self):
- """Return the unique ID of the switch."""
- return self._unique_id
-
- @property
- def device_info(self):
- """Return the device info."""
- return {
- "identifiers": {(DOMAIN, self._device_instance.uid)},
- "name": self.name,
- "manufacturer": self._brand,
- "model": self._model,
- }
-
- @property
- def device_id(self):
- """Return the ID of this switch."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the display name of this switch."""
- return self._name
-
- @property
- def should_poll(self):
- """Return the polling state."""
- return False
-
@property
def is_on(self):
"""Return the state."""
@@ -115,11 +65,6 @@ def current_power_w(self):
"""Return the current consumption."""
return self._consumption
- @property
- def available(self):
- """Return the online state."""
- return self._available
-
def turn_on(self, **kwargs):
"""Switch on the device."""
self._is_on = True
@@ -130,7 +75,7 @@ def turn_off(self, **kwargs):
self._is_on = False
self._binary_switch_property.set(state=False)
- def sync(self, message=None):
+ def _sync(self, message):
"""Update the binary switch state and consumption."""
if message[0].startswith("devolo.BinarySwitch"):
self._is_on = self._device_instance.binary_switch_property[message[0]].state
@@ -138,22 +83,6 @@ def sync(self, message=None):
self._consumption = self._device_instance.consumption_property[
message[0]
].current
- elif message[0].startswith("hdm"):
- self._available = self._device_instance.is_online()
else:
- _LOGGER.debug("No valid message received: %s", message)
+ self._generic_message(message)
self.schedule_update_ha_state()
-
-
-class Subscriber:
- """Subscriber class for the publisher in mprm websocket class."""
-
- def __init__(self, name, callback):
- """Initiate the device."""
- self.name = name
- self.callback = callback
-
- def update(self, message):
- """Trigger hass to update the device."""
- _LOGGER.debug('%s got message "%s"', self.name, message)
- self.callback(message)
diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json
index af0b4eb105ac49..593c50980144ab 100644
--- a/homeassistant/components/devolo_home_control/translations/ca.json
+++ b/homeassistant/components/devolo_home_control/translations/ca.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquesta Central Unit de Home Control ja est\u00e0 en \u00fas."
+ "already_configured": "El compte ja ha estat configurat"
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Nom d'usuari i/o contrasenya incorrectes."
},
"step": {
diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json
index ae888d37f4a7b8..e5c9f84cab0274 100644
--- a/homeassistant/components/devolo_home_control/translations/en.json
+++ b/homeassistant/components/devolo_home_control/translations/en.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "This Home Control Central Unit is already in use."
+ "already_configured": "Account is already configured"
},
"error": {
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Incorrect user name and/or password."
},
"step": {
@@ -12,7 +13,7 @@
"home_control_url": "Home Control URL",
"mydevolo_url": "mydevolo URL",
"password": "Password",
- "username": "E-Mail-Address / devolo ID"
+ "username": "Email / devolo ID"
},
"title": "devolo Home Control"
}
diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json
index 9eb7f04f923ac2..d511b5d28036e3 100644
--- a/homeassistant/components/devolo_home_control/translations/es.json
+++ b/homeassistant/components/devolo_home_control/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "Esta Unidad Central de Home Control ya est\u00e1 en uso."
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Nombre de usuario y/o contrase\u00f1a incorrectos."
},
"step": {
diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json
new file mode 100644
index 00000000000000..2227b7442a79c6
--- /dev/null
+++ b/homeassistant/components/devolo_home_control/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json
index 1f9b6f4e28f34d..acc2b171b638cf 100644
--- a/homeassistant/components/devolo_home_control/translations/fr.json
+++ b/homeassistant/components/devolo_home_control/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e."
},
"error": {
+ "invalid_auth": "Authentification invalide",
"invalid_credentials": "Nom d''utilisateur et/ou mot de passe incorrect."
},
"step": {
diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json
index 89bc030a7c84c9..8788e3e307afdc 100644
--- a/homeassistant/components/devolo_home_control/translations/it.json
+++ b/homeassistant/components/devolo_home_control/translations/it.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo Home Control Central \u00e8 gi\u00e0 in uso."
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Nome utente e/o password non corretti."
},
"step": {
@@ -12,7 +13,7 @@
"home_control_url": "URL di Home Control",
"mydevolo_url": "URL di mydevolo",
"password": "Password",
- "username": "Indirizzo e-mail / devolo ID"
+ "username": "E-mail / devolo ID"
},
"title": "devolo Home Control"
}
diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json
index 19c8d2653c1f55..2ec2b0744f24b5 100644
--- a/homeassistant/components/devolo_home_control/translations/no.json
+++ b/homeassistant/components/devolo_home_control/translations/no.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Denne hjemmekontrollsentralenheten er allerede i bruk."
+ "already_configured": "Kontoen er allerede konfigurert"
},
"error": {
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_credentials": "Ugyldig brukernavn og/eller passord"
},
"step": {
@@ -12,7 +13,7 @@
"home_control_url": "Home Control URL",
"mydevolo_url": "mydevolo URL",
"password": "Passord",
- "username": "E-postadresse / devolo-ID"
+ "username": "E-post / devolo ID"
},
"title": ""
}
diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json
index b65a45d72275ad..71cff94add67c7 100644
--- a/homeassistant/components/devolo_home_control/translations/ru.json
+++ b/homeassistant/components/devolo_home_control/translations/ru.json
@@ -1,16 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u043e\u0442 Home Control Central Unit \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
+ "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": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
"user": {
"data": {
- "home_control_url": "Home Control URL",
- "mydevolo_url": "mydevolo URL",
+ "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441",
+ "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID"
},
diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json
index ef2407fe5bd9c5..3336d67464a16a 100644
--- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json
+++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json
@@ -1,18 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64 Home Control Central \u5df2\u7d93\u65bc\u4f7f\u7528\u4e2d\u3002"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u53ca/\u6216\u5bc6\u78bc\u932f\u8aa4\u3002"
},
"step": {
"user": {
"data": {
- "home_control_url": "Home Control URL",
- "mydevolo_url": "mydevolo URL",
+ "home_control_url": "Home Control \u7db2\u5740",
+ "mydevolo_url": "mydevolo \u7db2\u5740",
"password": "\u5bc6\u78bc",
- "username": "E-Mail \u4f4d\u5740 / devolo ID"
+ "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID"
},
"title": "devolo Home Control"
}
diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py
index e2ce9e491863d2..26c0c92ca78061 100644
--- a/homeassistant/components/dexcom/config_flow.py
+++ b/homeassistant/components/dexcom/config_flow.py
@@ -46,9 +46,9 @@ async def async_step_user(self, user_input=None):
user_input[CONF_SERVER] == SERVER_OUS,
)
except SessionError:
- errors["base"] = "session_error"
+ errors["base"] = "cannot_connect"
except AccountError:
- errors["base"] = "account_error"
+ errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json
index 7b9932ec4de6af..bb29f814ee8619 100644
--- a/homeassistant/components/dexcom/strings.json
+++ b/homeassistant/components/dexcom/strings.json
@@ -12,11 +12,13 @@
}
},
"error": {
- "session_error": "[%key:common::config_flow::error::cannot_connect%]",
- "account_error": "[%key:common::config_flow::error::invalid_auth%]",
+ "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_account": "[%key:common::config_flow::abort::already_configured_account%]" }
+ "abort": {
+ "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]"
+ }
},
"options": {
"step": {
diff --git a/homeassistant/components/dexcom/translations/ca.json b/homeassistant/components/dexcom/translations/ca.json
index 7be3b94993f8bb..b92d6b7ab06e55 100644
--- a/homeassistant/components/dexcom/translations/ca.json
+++ b/homeassistant/components/dexcom/translations/ca.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "Autenticaci\u00f3 inv\u00e0lida",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"session_error": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json
index af843097539ac9..31ded6b7f9e018 100644
--- a/homeassistant/components/dexcom/translations/de.json
+++ b/homeassistant/components/dexcom/translations/de.json
@@ -10,7 +10,8 @@
"step": {
"user": {
"data": {
- "password": "Passwort"
+ "password": "Passwort",
+ "username": "Benutzername"
}
}
}
diff --git a/homeassistant/components/dexcom/translations/el.json b/homeassistant/components/dexcom/translations/el.json
new file mode 100644
index 00000000000000..b30be708065e42
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/el.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/en.json b/homeassistant/components/dexcom/translations/en.json
index 2d2f6aa0834b88..92ca2d11859450 100644
--- a/homeassistant/components/dexcom/translations/en.json
+++ b/homeassistant/components/dexcom/translations/en.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "Invalid authentication",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"session_error": "Failed to connect",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json
index 146e77632514f3..95f80f558b3d28 100644
--- a/homeassistant/components/dexcom/translations/es.json
+++ b/homeassistant/components/dexcom/translations/es.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "Autenticaci\u00f3n no v\u00e1lida",
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"session_error": "No se pudo conectar",
"unknown": "Error inesperado"
},
diff --git a/homeassistant/components/dexcom/translations/et.json b/homeassistant/components/dexcom/translations/et.json
new file mode 100644
index 00000000000000..1632ade0fe242c
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Tuvastamise viga"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "M\u00f5\u00f5t\u00fchik"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json
index 3825a88d322c64..5ad342c22ffc94 100644
--- a/homeassistant/components/dexcom/translations/fr.json
+++ b/homeassistant/components/dexcom/translations/fr.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "L'authentification ne'st pas valide",
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
"session_error": "\u00c9chec de connexion",
"unknown": "Erreur inattendue"
},
diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json
new file mode 100644
index 00000000000000..9f2fd5d72f4336
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/it.json b/homeassistant/components/dexcom/translations/it.json
index e9695304c92322..0042c585cd1a1e 100644
--- a/homeassistant/components/dexcom/translations/it.json
+++ b/homeassistant/components/dexcom/translations/it.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "Autenticazione non valida",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"session_error": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/dexcom/translations/nl.json b/homeassistant/components/dexcom/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/no.json b/homeassistant/components/dexcom/translations/no.json
index 61ad015b5a49c5..4df58b25f1c899 100644
--- a/homeassistant/components/dexcom/translations/no.json
+++ b/homeassistant/components/dexcom/translations/no.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "Ugyldig godkjenning",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
"session_error": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/dexcom/translations/pl.json b/homeassistant/components/dexcom/translations/pl.json
index 24ae7a17370fbc..be45bb9f6136fb 100644
--- a/homeassistant/components/dexcom/translations/pl.json
+++ b/homeassistant/components/dexcom/translations/pl.json
@@ -1,12 +1,13 @@
{
"config": {
"abort": {
- "already_configured_account": "Konto jest ju\u017c skonfigurowane."
+ "already_configured_account": "Konto jest ju\u017c skonfigurowane"
},
"error": {
- "account_error": "Niepoprawne uwierzytelnienie.",
- "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "account_error": "Niepoprawne uwierzytelnienie",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json
index 01bd9a3f0b3376..69b79638100ef7 100644
--- a/homeassistant/components/dexcom/translations/ru.json
+++ b/homeassistant/components/dexcom/translations/ru.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"session_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/dexcom/translations/zh-Hant.json b/homeassistant/components/dexcom/translations/zh-Hant.json
index f056b92b2de1b9..656e082f7c4a08 100644
--- a/homeassistant/components/dexcom/translations/zh-Hant.json
+++ b/homeassistant/components/dexcom/translations/zh-Hant.json
@@ -5,6 +5,8 @@
},
"error": {
"account_error": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"session_error": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/dialogflow/strings.json b/homeassistant/components/dialogflow/strings.json
index d1a691dc92bf6b..f17491a75285bd 100644
--- a/homeassistant/components/dialogflow/strings.json
+++ b/homeassistant/components/dialogflow/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages."
},
"create_entry": {
diff --git a/homeassistant/components/dialogflow/translations/ca.json b/homeassistant/components/dialogflow/translations/ca.json
index 17dd38ffb203f8..bc62fe1f555c14 100644
--- a/homeassistant/components/dialogflow/translations/ca.json
+++ b/homeassistant/components/dialogflow/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Dialogflow.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Completa la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/json\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/dialogflow/translations/el.json b/homeassistant/components/dialogflow/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/en.json b/homeassistant/components/dialogflow/translations/en.json
index cc9eda6a96803e..f78843fd5034ea 100644
--- a/homeassistant/components/dialogflow/translations/en.json
+++ b/homeassistant/components/dialogflow/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/dialogflow/translations/es.json b/homeassistant/components/dialogflow/translations/es.json
index f71313484994e1..3d6f9c440e8f81 100644
--- a/homeassistant/components/dialogflow/translations/es.json
+++ b/homeassistant/components/dialogflow/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json
new file mode 100644
index 00000000000000..fafda5352ec1b4
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/et.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Dialogflow teavituste vastuv\u00f5tmiseks peab teie Home Assistant olema Interneti kaudu ligip\u00e4\u00e4setav.",
+ "one_instance_allowed": "Vaja on ainult \u00fchte \u00fcksust.",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "create_entry": {
+ "default": "S\u00fcndmuste saatmiseks Home Assistantile peate seadistama [Dialogflow'i veebihaagii integreerimine] ( {dialogflow_url} ). \n\n Sisestage j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n - Sisu t\u00fc\u00fcp: rakendus / json \n\n Lisateavet leiate [dokumentatsioonist] ( {docs_url} )."
+ },
+ "step": {
+ "user": {
+ "description": "Kas oled kindel, et soovid seadistada Dialogflow?",
+ "title": "Seadistage Dialogflow veebihaak"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/fr.json b/homeassistant/components/dialogflow/translations/fr.json
index 81de11edbd5bb6..85ace2c378e643 100644
--- a/homeassistant/components/dialogflow/translations/fr.json
+++ b/homeassistant/components/dialogflow/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages Dialogflow.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Dialogflow] ( {dialogflow_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes."
diff --git a/homeassistant/components/dialogflow/translations/it.json b/homeassistant/components/dialogflow/translations/it.json
index 2c933d09a523bd..a318633d4e4eaa 100644
--- a/homeassistant/components/dialogflow/translations/it.json
+++ b/homeassistant/components/dialogflow/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Dialogflow.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli."
diff --git a/homeassistant/components/dialogflow/translations/lb.json b/homeassistant/components/dialogflow/translations/lb.json
index a10adda6702526..d64fa8bd2e7ff0 100644
--- a/homeassistant/components/dialogflow/translations/lb.json
+++ b/homeassistant/components/dialogflow/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Dialogflow Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss [Webhook Integratioun mat Dialogflow]({dialogflow_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/dialogflow/translations/no.json b/homeassistant/components/dialogflow/translations/no.json
index 8abbc221b7cc19..91d4d53f13e05a 100644
--- a/homeassistant/components/dialogflow/translations/no.json
+++ b/homeassistant/components/dialogflow/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta Dialogflow meldinger.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [webhook integrasjon av Dialogflow]({dialogflow_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) for ytterligere detaljer."
diff --git a/homeassistant/components/dialogflow/translations/ru.json b/homeassistant/components/dialogflow/translations/ru.json
index 15ecb63392e0a3..7e0d0087ec76a4 100644
--- a/homeassistant/components/dialogflow/translations/ru.json
+++ b/homeassistant/components/dialogflow/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Dialogflow.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\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]({docs_url}) \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."
diff --git a/homeassistant/components/dialogflow/translations/zh-Hant.json b/homeassistant/components/dialogflow/translations/zh-Hant.json
index bf850322480550..c03cfbfd72d69f 100644
--- a/homeassistant/components/dialogflow/translations/zh-Hant.json
+++ b/homeassistant/components/dialogflow/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [webhook integration of Dialogflow]({dialogflow_url})\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py
index d076dae9210f2d..eb1345df45c011 100644
--- a/homeassistant/components/digital_ocean/binary_sensor.py
+++ b/homeassistant/components/digital_ocean/binary_sensor.py
@@ -3,7 +3,11 @@
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOVING,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv
@@ -25,7 +29,6 @@
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Droplet"
-DEFAULT_DEVICE_CLASS = "moving"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])}
)
@@ -73,7 +76,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this sensor."""
- return DEFAULT_DEVICE_CLASS
+ return DEVICE_CLASS_MOVING
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py
index 7448b9fbcf3847..f86f6d7f1aa553 100644
--- a/homeassistant/components/digitalloggers/switch.py
+++ b/homeassistant/components/digitalloggers/switch.py
@@ -95,11 +95,6 @@ def is_on(self):
"""Return true if relay is on."""
return self._state
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
def turn_on(self, **kwargs):
"""Instruct the relay to turn on."""
self._outlet.on()
diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py
index 127fb5c04b26b7..dfd88ca885b5ff 100644
--- a/homeassistant/components/directv/media_player.py
+++ b/homeassistant/components/directv/media_player.py
@@ -126,14 +126,14 @@ async def async_update(self):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
- attributes = {}
- if not self._is_standby:
- attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = self.media_currently_recording
- attributes[ATTR_MEDIA_RATING] = self.media_rating
- attributes[ATTR_MEDIA_RECORDED] = self.media_recorded
- attributes[ATTR_MEDIA_START_TIME] = self.media_start_time
-
- return attributes
+ if self._is_standby:
+ return {}
+ return {
+ ATTR_MEDIA_CURRENTLY_RECORDING: self.media_currently_recording,
+ ATTR_MEDIA_RATING: self.media_rating,
+ ATTR_MEDIA_RECORDED: self.media_recorded,
+ ATTR_MEDIA_START_TIME: self.media_start_time,
+ }
@property
def name(self):
diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json
index 7a07185978beaf..9e30a366cc07f9 100644
--- a/homeassistant/components/directv/strings.json
+++ b/homeassistant/components/directv/strings.json
@@ -17,7 +17,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "unknown": "Unexpected error"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}
diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json
index bec0198ca704b0..db0dc7ea0a42d8 100644
--- a/homeassistant/components/directv/translations/pl.json
+++ b/homeassistant/components/directv/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"flow_title": "DirecTV: {name}",
"step": {
diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py
index 4fd21f200dd062..c173c879ad1808 100644
--- a/homeassistant/components/dlink/switch.py
+++ b/homeassistant/components/dlink/switch.py
@@ -13,7 +13,6 @@
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
- STATE_UNKNOWN,
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
@@ -146,14 +145,14 @@ def update(self):
_LOGGER.warning("Waiting %s s to retry", retry_seconds)
return
- _state = STATE_UNKNOWN
+ _state = "unknown"
try:
self._last_tried = dt_util.now()
_state = self.smartplug.state
except urllib.error.HTTPError:
_LOGGER.error("D-Link connection problem")
- if _state == STATE_UNKNOWN:
+ if _state == "unknown":
self._n_tried += 1
self.available = False
_LOGGER.warning("Failed to connect to D-Link switch")
diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
index 4130f67ec138d2..f4180ffcffa5d1 100644
--- a/homeassistant/components/doods/image_processing.py
+++ b/homeassistant/components/doods/image_processing.py
@@ -1,6 +1,7 @@
"""Support for the DOODS service."""
import io
import logging
+import os
import time
from PIL import Image, ImageDraw, UnidentifiedImageError
@@ -26,6 +27,7 @@
ATTR_MATCHES = "matches"
ATTR_SUMMARY = "summary"
ATTR_TOTAL_MATCHES = "total_matches"
+ATTR_PROCESS_TIME = "process_time"
CONF_URL = "url"
CONF_AUTH_KEY = "auth_key"
@@ -203,6 +205,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config):
self._matches = {}
self._total_matches = 0
self._last_image = None
+ self._process_time = 0
@property
def camera_entity(self):
@@ -228,6 +231,7 @@ def device_state_attributes(self):
label: len(values) for label, values in self._matches.items()
},
ATTR_TOTAL_MATCHES: self._total_matches,
+ ATTR_PROCESS_TIME: self._process_time,
}
def _save_image(self, image, matches, paths):
@@ -270,6 +274,8 @@ def _save_image(self, image, matches, paths):
for path in paths:
_LOGGER.info("Saving results image to %s", path)
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
img.save(path)
def process_image(self, image):
@@ -308,6 +314,7 @@ def process_image(self, image):
_LOGGER.error(response["error"])
self._matches = matches
self._total_matches = total_matches
+ self._process_time = time.monotonic() - start
return
for detection in response["detections"]:
@@ -380,3 +387,4 @@ def process_image(self, image):
self._matches = matches
self._total_matches = total_matches
+ self._process_time = time.monotonic() - start
diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py
index 43ab0c96153e05..1f7e02e8569b25 100644
--- a/homeassistant/components/doorbird/__init__.py
+++ b/homeassistant/components/doorbird/__init__.py
@@ -19,6 +19,7 @@
CONF_TOKEN,
CONF_USERNAME,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
@@ -127,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
status = await hass.async_add_executor_job(device.ready)
info = await hass.async_add_executor_job(device.info)
except urllib.error.HTTPError as err:
- if err.code == 401:
+ if err.code == HTTP_UNAUTHORIZED:
_LOGGER.error(
"Authorization rejected by DoorBird for %s@%s", username, device_ip
)
@@ -357,7 +358,9 @@ async def get(self, request, event):
device = get_doorstation_by_token(hass, token)
if device is None:
- return web.Response(status=401, text="Invalid token provided.")
+ return web.Response(
+ status=HTTP_UNAUTHORIZED, text="Invalid token provided."
+ )
if device:
event_data = device.get_event_data()
diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py
index 07b753da6ee8a3..8e3f661254df15 100644
--- a/homeassistant/components/doorbird/config_flow.py
+++ b/homeassistant/components/doorbird/config_flow.py
@@ -7,7 +7,13 @@
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.core import callback
from homeassistant.util.network import is_link_local
@@ -39,7 +45,7 @@ async def validate_input(hass: core.HomeAssistant, data):
status = await hass.async_add_executor_job(device.ready)
info = await hass.async_add_executor_job(device.info)
except urllib.error.HTTPError as err:
- if err.code == 401:
+ if err.code == HTTP_UNAUTHORIZED:
raise InvalidAuth from err
raise CannotConnect from err
except OSError as err:
diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json
index 6f1f866053e231..076124cf0954e3 100644
--- a/homeassistant/components/doorbird/strings.json
+++ b/homeassistant/components/doorbird/strings.json
@@ -22,7 +22,7 @@
}
},
"abort": {
- "already_configured": "This DoorBird is already configured",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird"
},
@@ -33,4 +33,4 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json
index 1639b471d4d864..2adc6227ab4a93 100644
--- a/homeassistant/components/doorbird/translations/ca.json
+++ b/homeassistant/components/doorbird/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu DoorBird ja est\u00e0 configurat",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"link_local_address": "L'enlla\u00e7 amb adreces locals no est\u00e0 perm\u00e8s",
"not_doorbird_device": "Aquest dispositiu no \u00e9s DoorBird"
},
diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json
index adf3127ffa0669..8ddf95f377380f 100644
--- a/homeassistant/components/doorbird/translations/en.json
+++ b/homeassistant/components/doorbird/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "This DoorBird is already configured",
+ "already_configured": "Device is already configured",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird"
},
diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json
index 304760dbf58e45..fd8bf04d29e32c 100644
--- a/homeassistant/components/doorbird/translations/fr.json
+++ b/homeassistant/components/doorbird/translations/fr.json
@@ -28,7 +28,8 @@
"init": {
"data": {
"events": "Liste d'\u00e9v\u00e9nements s\u00e9par\u00e9s par des virgules."
- }
+ },
+ "description": "Ajoutez un nom d'\u00e9v\u00e9nement s\u00e9par\u00e9 par des virgules pour chaque \u00e9v\u00e9nement que vous souhaitez suivre. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique. Consultez la documentation sur https://www.home-assistant.io/integrations/doorbird/#events. Exemple: somebody_pressed_the_button, motion"
}
}
}
diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json
index ee9c603fb13ec6..51b45cb79bb273 100644
--- a/homeassistant/components/doorbird/translations/it.json
+++ b/homeassistant/components/doorbird/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Questo DoorBird \u00e8 gi\u00e0 configurato",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"link_local_address": "Gli indirizzi locali di collegamento non sono supportati",
"not_doorbird_device": "Questo dispositivo non \u00e8 un DoorBird"
},
diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json
index 85180df8b4a1d0..2bf97d687ab851 100644
--- a/homeassistant/components/doorbird/translations/nl.json
+++ b/homeassistant/components/doorbird/translations/nl.json
@@ -13,7 +13,8 @@
"user": {
"data": {
"host": "Host (IP-adres)",
- "name": "Apparaatnaam"
+ "name": "Apparaatnaam",
+ "username": "Gebruikersnaam"
},
"title": "Maak verbinding met de DoorBird"
}
diff --git a/homeassistant/components/doorbird/translations/no.json b/homeassistant/components/doorbird/translations/no.json
index 4929e58c61fe52..8b48beef43e068 100644
--- a/homeassistant/components/doorbird/translations/no.json
+++ b/homeassistant/components/doorbird/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Denne DoorBird er allerede konfigurert",
+ "already_configured": "Enheten er allerede konfigurert",
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
"not_doorbird_device": "Denne enheten er ikke en DoorBird"
},
diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json
index a24febcd94afed..446fd21626a6ad 100644
--- a/homeassistant/components/doorbird/translations/pl.json
+++ b/homeassistant/components/doorbird/translations/pl.json
@@ -6,9 +6,9 @@
"not_doorbird_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem DoorBird."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "DoorBird {name} ({host})",
"step": {
diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json
index 9ca74bd778059f..274b88a8b473f1 100644
--- a/homeassistant/components/doorbird/translations/ru.json
+++ b/homeassistant/components/doorbird/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
"link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.",
"not_doorbird_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 DoorBird."
},
diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json
index 281a6e54ac2361..a4b3bd2fd8686a 100644
--- a/homeassistant/components/doorbird/translations/zh-Hant.json
+++ b/homeassistant/components/doorbird/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740",
"not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird"
},
diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py
index d0d0304a02a562..724f9393fbfaa1 100644
--- a/homeassistant/components/dsmr/config_flow.py
+++ b/homeassistant/components/dsmr/config_flow.py
@@ -48,9 +48,9 @@ async def validate_connect(self, hass: core.HomeAssistant) -> bool:
"""Test if we can validate connection with the device."""
def update_telegram(telegram):
- self._telegram = telegram
-
- transport.close()
+ if obis_ref.EQUIPMENT_IDENTIFIER in telegram:
+ self._telegram = telegram
+ transport.close()
if self._host is None:
reader_factory = partial(
diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json
new file mode 100644
index 00000000000000..e8e23bf8343ee3
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/es.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json
index c4bc0d48b1a08b..ea382532a71327 100644
--- a/homeassistant/components/dsmr/translations/fr.json
+++ b/homeassistant/components/dsmr/translations/fr.json
@@ -2,6 +2,10 @@
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "step": {
+ "one": "",
+ "other": "Autre"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/ko.json b/homeassistant/components/dsmr/translations/ko.json
new file mode 100644
index 00000000000000..9c8fbbe80a99cb
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/ko.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/lb.json b/homeassistant/components/dsmr/translations/lb.json
new file mode 100644
index 00000000000000..6469543442e1ff
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/lb.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json
index 815a6f19706e95..637a81a3f87144 100644
--- a/homeassistant/components/dsmr/translations/pl.json
+++ b/homeassistant/components/dsmr/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py
index 0ec67bc97fd8f2..5fda67e65a342c 100644
--- a/homeassistant/components/dsmr_reader/definitions.py
+++ b/homeassistant/components/dsmr_reader/definitions.py
@@ -1,6 +1,7 @@
"""Definitions for DSMR Reader sensors added to MQTT."""
from homeassistant.const import (
+ CURRENCY_EURO,
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
VOLT,
@@ -166,17 +167,17 @@ def tariff_transform(value):
"dsmr/day-consumption/electricity1_cost": {
"name": "Low tariff cost",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/electricity2_cost": {
"name": "High tariff cost",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/electricity_cost_merged": {
"name": "Power total cost",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/gas": {
"name": "Gas usage",
@@ -186,37 +187,37 @@ def tariff_transform(value):
"dsmr/day-consumption/gas_cost": {
"name": "Gas cost",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/total_cost": {
"name": "Total cost",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": {
"name": "Low tariff delivered price",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": {
"name": "High tariff delivered price",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_returned_1": {
"name": "Low tariff returned price",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_returned_2": {
"name": "High tariff returned price",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_gas": {
"name": "Gas price",
"icon": "mdi:currency-eur",
- "unit": "€",
+ "unit": CURRENCY_EURO,
},
"dsmr/meter-stats/dsmr_version": {
"name": "DSMR version",
diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json
new file mode 100644
index 00000000000000..44b4442dc31c9e
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/hu.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json
index 2e0e2b352ca830..893a71d4b701a6 100644
--- a/homeassistant/components/dunehd/translations/pl.json
+++ b/homeassistant/components/dunehd/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP."
},
"step": {
diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json
index e67fbb08e29227..df4c412cc62842 100644
--- a/homeassistant/components/dwd_weather_warnings/manifest.json
+++ b/homeassistant/components/dwd_weather_warnings/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "dwd_weather_warnings",
- "name": "Deutsche Wetter Dienst (DWD) Weather Warnings",
+ "name": "Deutscher Wetterdienst (DWD) Weather Warnings",
"documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings",
"codeowners": ["@runningman84", "@stephan192", "@Hummel95"],
- "requirements": ["dwdwfsapi==1.0.2"]
+ "requirements": ["dwdwfsapi==1.0.3"]
}
diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py
index db985e57a41bdb..c076fc81628bb6 100644
--- a/homeassistant/components/dweet/__init__.py
+++ b/homeassistant/components/dweet/__init__.py
@@ -6,6 +6,7 @@
import voluptuous as vol
from homeassistant.const import (
+ ATTR_FRIENDLY_NAME,
CONF_NAME,
CONF_WHITELIST,
EVENT_STATE_CHANGED,
@@ -58,7 +59,7 @@ def dweet_event_listener(event):
except ValueError:
_state = state.state
- json_body[state.attributes.get("friendly_name")] = _state
+ json_body[state.attributes.get(ATTR_FRIENDLY_NAME)] = _state
send_data(name, json_body)
diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/eafm/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/ko.json b/homeassistant/components/eafm/translations/ko.json
new file mode 100644
index 00000000000000..4e7bfc9dc9363d
--- /dev/null
+++ b/homeassistant/components/eafm/translations/ko.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "\uc2a4\ud14c\uc774\uc158"
+ },
+ "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd",
+ "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/pl.json b/homeassistant/components/eafm/translations/pl.json
new file mode 100644
index 00000000000000..54f53bf5e32050
--- /dev/null
+++ b/homeassistant/components/eafm/translations/pl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "no_stations": "Nie znaleziono stacji monitorowania powodzi."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "Stacja"
+ },
+ "description": "Wybierz stacj\u0119, kt\u00f3r\u0105 chcesz monitorowa\u0107",
+ "title": "\u015aled\u017a stacj\u0119 monitorowania powodzi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py
index 855e62727b5683..00c40344d6ec28 100644
--- a/homeassistant/components/ebusd/__init__.py
+++ b/homeassistant/components/ebusd/__init__.py
@@ -1,5 +1,4 @@
"""Support for Ebusd daemon for communication with eBUS heating systems."""
-from datetime import timedelta
import logging
import socket
@@ -14,7 +13,6 @@
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
-from homeassistant.util import Throttle
from .const import DOMAIN, SENSOR_TYPES
@@ -26,8 +24,6 @@
CACHE_TTL = 900
SERVICE_EBUSD_WRITE = "ebusd_write"
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15)
-
def verify_ebusd_config(config):
"""Verify eBusd config."""
@@ -59,6 +55,7 @@ def verify_ebusd_config(config):
def setup(hass, config):
"""Set up the eBusd component."""
+ _LOGGER.debug("Integration setup started")
conf = config[DOMAIN]
name = conf[CONF_NAME]
circuit = conf[CONF_CIRCUIT]
@@ -66,7 +63,6 @@ def setup(hass, config):
server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT))
try:
- _LOGGER.debug("Ebusd integration setup started")
ebusdpy.init(server_address)
hass.data[DOMAIN] = EbusdData(server_address, circuit)
@@ -95,7 +91,6 @@ def __init__(self, address, circuit):
self._address = address
self.value = {}
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, name, stype):
"""Call the Ebusd API to update the data."""
try:
diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py
index 63f72a89ccd8aa..badb94a6f8541f 100644
--- a/homeassistant/components/ebusd/sensor.py
+++ b/homeassistant/components/ebusd/sensor.py
@@ -3,6 +3,7 @@
import logging
from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
from .const import DOMAIN
@@ -13,6 +14,7 @@
TIME_FRAME2_END = "time_frame2_end"
TIME_FRAME3_BEGIN = "time_frame3_begin"
TIME_FRAME3_END = "time_frame3_end"
+MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=15)
_LOGGER = logging.getLogger(__name__)
@@ -85,6 +87,7 @@ def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Fetch new state data for the sensor."""
try:
diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py
index 64c4b07ed1f0a1..9593fc0e497c2e 100644
--- a/homeassistant/components/ecobee/binary_sensor.py
+++ b/homeassistant/components/ecobee/binary_sensor.py
@@ -70,7 +70,7 @@ def device_info(self):
_LOGGER.error(
"Model number for ecobee thermostat %s not recognized. "
"Please visit this link and provide the following information: "
- "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "https://github.com/home-assistant/core/issues/27172 "
"Unrecognized model number: %s",
thermostat["name"],
thermostat["modelNumber"],
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index dcb21ac41b187c..ccfddca4b03015 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -351,7 +351,7 @@ def device_info(self):
_LOGGER.error(
"Model number for ecobee thermostat %s not recognized. "
"Please visit this link and provide the following information: "
- "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "https://github.com/home-assistant/core/issues/27172 "
"Unrecognized model number: %s",
self.name,
self.thermostat["modelNumber"],
diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py
index cbe16832a34781..6641181b7e4cd1 100644
--- a/homeassistant/components/ecobee/config_flow.py
+++ b/homeassistant/components/ecobee/config_flow.py
@@ -29,7 +29,7 @@ async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
# Config entry already exists, only one allowed.
- return self.async_abort(reason="one_instance_only")
+ return self.async_abort(reason="single_instance_allowed")
errors = {}
stored_api_key = (
diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py
index 4351b230538612..cfbaa7a45161b0 100644
--- a/homeassistant/components/ecobee/sensor.py
+++ b/homeassistant/components/ecobee/sensor.py
@@ -5,7 +5,6 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
- STATE_UNKNOWN,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers.entity import Entity
@@ -83,7 +82,7 @@ def device_info(self):
_LOGGER.error(
"Model number for ecobee thermostat %s not recognized. "
"Please visit this link and provide the following information: "
- "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "https://github.com/home-assistant/core/issues/27172 "
"Unrecognized model number: %s",
thermostat["name"],
thermostat["modelNumber"],
@@ -112,7 +111,7 @@ def state(self):
if self._state in [
ECOBEE_STATE_CALIBRATING,
ECOBEE_STATE_UNKNOWN,
- STATE_UNKNOWN,
+ "unknown",
]:
return None
diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json
index b80996cb2a31a3..78f0708134c8f6 100644
--- a/homeassistant/components/ecobee/strings.json
+++ b/homeassistant/components/ecobee/strings.json
@@ -18,7 +18,7 @@
"token_request_failed": "Error requesting tokens from ecobee; please try again."
},
"abort": {
- "one_instance_only": "This integration currently supports only one ecobee instance."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/ecobee/translations/ca.json b/homeassistant/components/ecobee/translations/ca.json
index b75006483c7e7d..8b20669c76ba9d 100644
--- a/homeassistant/components/ecobee/translations/ca.json
+++ b/homeassistant/components/ecobee/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "Aquesta integraci\u00f3 nom\u00e9s admet una sola inst\u00e0ncia ecobee."
+ "one_instance_only": "Aquesta integraci\u00f3 nom\u00e9s admet una sola inst\u00e0ncia ecobee.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
"pin_request_failed": "Error al sol\u00b7licitar els PIN d'ecobee; verifica que la clau API \u00e9s correcta.",
diff --git a/homeassistant/components/ecobee/translations/en.json b/homeassistant/components/ecobee/translations/en.json
index a105296f813e0e..024ac774133af7 100644
--- a/homeassistant/components/ecobee/translations/en.json
+++ b/homeassistant/components/ecobee/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "This integration currently supports only one ecobee instance."
+ "one_instance_only": "This integration currently supports only one ecobee instance.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.",
diff --git a/homeassistant/components/ecobee/translations/es.json b/homeassistant/components/ecobee/translations/es.json
index 26260e38ca7fea..dd6b9526f9b5c2 100644
--- a/homeassistant/components/ecobee/translations/es.json
+++ b/homeassistant/components/ecobee/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee."
+ "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
"pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.",
diff --git a/homeassistant/components/ecobee/translations/et.json b/homeassistant/components/ecobee/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/ecobee/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/translations/it.json b/homeassistant/components/ecobee/translations/it.json
index dce66271b9ac85..b243662201082c 100644
--- a/homeassistant/components/ecobee/translations/it.json
+++ b/homeassistant/components/ecobee/translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "Questa integrazione supporta attualmente una sola istanza ecobee."
+ "one_instance_only": "Questa integrazione supporta attualmente una sola istanza ecobee.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
"pin_request_failed": "Errore durante la richiesta del PIN da ecobee; verificare che la chiave API sia corretta.",
diff --git a/homeassistant/components/ecobee/translations/no.json b/homeassistant/components/ecobee/translations/no.json
index 048659ac83d190..0c634b39bd9ce6 100644
--- a/homeassistant/components/ecobee/translations/no.json
+++ b/homeassistant/components/ecobee/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst."
+ "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
"pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.",
diff --git a/homeassistant/components/ecobee/translations/ru.json b/homeassistant/components/ecobee/translations/ru.json
index 37c1f63822c22b..f9c12b5b3a3a70 100644
--- a/homeassistant/components/ecobee/translations/ru.json
+++ b/homeassistant/components/ecobee/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_only": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 ecobee."
+ "one_instance_only": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 ecobee.",
+ "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": {
"pin_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 PIN-\u043a\u043e\u0434\u0430 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.",
diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py
index 4ea90d27106344..7774a6648a5c19 100644
--- a/homeassistant/components/ecobee/weather.py
+++ b/homeassistant/components/ecobee/weather.py
@@ -1,5 +1,5 @@
"""Support for displaying weather info from Ecobee API."""
-from datetime import datetime
+from datetime import timedelta
from pyecobee.const import ECOBEE_STATE_UNKNOWN
@@ -13,6 +13,7 @@
WeatherEntity,
)
from homeassistant.const import TEMP_FAHRENHEIT
+from homeassistant.util import dt as dt_util
from .const import (
_LOGGER,
@@ -73,7 +74,7 @@ def device_info(self):
_LOGGER.error(
"Model number for ecobee thermostat %s not recognized. "
"Please visit this link and provide the following information: "
- "https://github.com/home-assistant/home-assistant/issues/27172 "
+ "https://github.com/home-assistant/core/issues/27172 "
"Unrecognized model number: %s",
thermostat["name"],
thermostat["modelNumber"],
@@ -165,10 +166,13 @@ def forecast(self):
return None
forecasts = []
- for day in range(1, 5):
+ date = dt_util.utcnow()
+ for day in range(0, 5):
forecast = _process_forecast(self.weather["forecasts"][day])
if forecast is None:
continue
+ forecast[ATTR_FORECAST_TIME] = date.isoformat()
+ date += timedelta(days=1)
forecasts.append(forecast)
if forecasts:
@@ -186,9 +190,6 @@ def _process_forecast(json):
"""Process a single ecobee API forecast to return expected values."""
forecast = {}
try:
- forecast[ATTR_FORECAST_TIME] = datetime.strptime(
- json["dateTime"], "%Y-%m-%d %H:%M:%S"
- ).isoformat()
forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[
json["weatherSymbol"]
]
diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py
index 0bc4c21fc2f14c..5eb7542fed9c7d 100644
--- a/homeassistant/components/edl21/sensor.py
+++ b/homeassistant/components/edl21/sensor.py
@@ -145,7 +145,7 @@ def event(self, message_body) -> None:
elif obis not in self._OBIS_BLACKLIST:
_LOGGER.warning(
"Unhandled sensor %s detected. Please report at "
- 'https://github.com/home-assistant/home-assistant/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+',
+ 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+',
obis,
)
self._OBIS_BLACKLIST.add(obis)
diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py
index 4be443a36f44bf..6882171b67f56a 100644
--- a/homeassistant/components/egardia/binary_sensor.py
+++ b/homeassistant/components/egardia/binary_sensor.py
@@ -1,7 +1,11 @@
"""Interfaces with Egardia/Woonveilig alarm control panel."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OPENING,
+ BinarySensorEntity,
+)
from homeassistant.const import STATE_OFF, STATE_ON
from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE
@@ -9,9 +13,9 @@
_LOGGER = logging.getLogger(__name__)
EGARDIA_TYPE_TO_DEVICE_CLASS = {
- "IR Sensor": "motion",
- "Door Contact": "opening",
- "IR": "motion",
+ "IR Sensor": DEVICE_CLASS_MOTION,
+ "Door Contact": DEVICE_CLASS_OPENING,
+ "IR": DEVICE_CLASS_MOTION,
}
diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py
index 824ea210d69eaf..43bcb4c2f0930f 100644
--- a/homeassistant/components/eight_sleep/sensor.py
+++ b/homeassistant/components/eight_sleep/sensor.py
@@ -112,11 +112,11 @@ async def async_update(self):
@property
def device_state_attributes(self):
"""Return device state attributes."""
- state_attr = {ATTR_TARGET_HEAT: self._usrobj.target_heating_level}
- state_attr[ATTR_ACTIVE_HEAT] = self._usrobj.now_heating
- state_attr[ATTR_DURATION_HEAT] = self._usrobj.heating_remaining
-
- return state_attr
+ return {
+ ATTR_TARGET_HEAT: self._usrobj.target_heating_level,
+ ATTR_ACTIVE_HEAT: self._usrobj.now_heating,
+ ATTR_DURATION_HEAT: self._usrobj.heating_remaining,
+ }
class EightUserSensor(EightSleepUserEntity):
diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py
index 687310c2c3eb73..624774b9bf42f3 100644
--- a/homeassistant/components/elgato/config_flow.py
+++ b/homeassistant/components/elgato/config_flow.py
@@ -33,7 +33,7 @@ async def async_step_user(
user_input[CONF_HOST], user_input[CONF_PORT]
)
except ElgatoError:
- return self._show_setup_form({"base": "connection_error"})
+ return self._show_setup_form({"base": "cannot_connect"})
# Check if already configured
await self.async_set_unique_id(info.serial_number)
@@ -53,14 +53,14 @@ async def async_step_zeroconf(
) -> Dict[str, Any]:
"""Handle zeroconf discovery."""
if user_input is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
try:
info = await self._get_elgato_info(
user_input[CONF_HOST], user_input[CONF_PORT]
)
except ElgatoError:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
# Check if already configured
await self.async_set_unique_id(info.serial_number)
@@ -92,7 +92,7 @@ async def async_step_zeroconf_confirm(
self.context.get(CONF_HOST), self.context.get(CONF_PORT)
)
except ElgatoError:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
# Check if already configured
await self.async_set_unique_id(info.serial_number)
diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json
index a00bf027451726..54c5f43a5dafbc 100644
--- a/homeassistant/components/elgato/strings.json
+++ b/homeassistant/components/elgato/strings.json
@@ -15,11 +15,11 @@
}
},
"error": {
- "connection_error": "Failed to connect to Elgato Key Light device."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "This Elgato Key Light device is already configured.",
- "connection_error": "Failed to connect to Elgato Key Light device."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/elgato/translations/ca.json b/homeassistant/components/elgato/translations/ca.json
index 1f02dc1f79fadf..04cf0488dc684c 100644
--- a/homeassistant/components/elgato/translations/ca.json
+++ b/homeassistant/components/elgato/translations/ca.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu Elgato Key Light ja est\u00e0 configurat.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/el.json b/homeassistant/components/elgato/translations/el.json
new file mode 100644
index 00000000000000..58012c1e4e38de
--- /dev/null
+++ b/homeassistant/components/elgato/translations/el.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/translations/en.json b/homeassistant/components/elgato/translations/en.json
index fea7945ecf4f2f..607ab996406c5c 100644
--- a/homeassistant/components/elgato/translations/en.json
+++ b/homeassistant/components/elgato/translations/en.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "This Elgato Key Light device is already configured.",
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to Elgato Key Light device."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to Elgato Key Light device."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json
index 46ced57ad2d4c0..60d872ef1c116e 100644
--- a/homeassistant/components/elgato/translations/es.json
+++ b/homeassistant/components/elgato/translations/es.json
@@ -2,9 +2,11 @@
"config": {
"abort": {
"already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.",
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se pudo conectar al dispositivo Elgato Key Light."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se pudo conectar al dispositivo Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/et.json b/homeassistant/components/elgato/translations/et.json
new file mode 100644
index 00000000000000..10864fe4d5d758
--- /dev/null
+++ b/homeassistant/components/elgato/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "Elgato Key Light seadmega \u00fchenduse loomine nurjus."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "Elgato Key Light seadmega \u00fchenduse loomine nurjus."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/translations/fr.json b/homeassistant/components/elgato/translations/fr.json
index 5a8e2bf46ca01b..7e0547166250d2 100644
--- a/homeassistant/components/elgato/translations/fr.json
+++ b/homeassistant/components/elgato/translations/fr.json
@@ -2,9 +2,11 @@
"config": {
"abort": {
"already_configured": "Cet appareil Elgato Key Light est d\u00e9j\u00e0 configur\u00e9.",
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/it.json b/homeassistant/components/elgato/translations/it.json
index 3985bcf6f591b1..2f1f2130a0bca6 100644
--- a/homeassistant/components/elgato/translations/it.json
+++ b/homeassistant/components/elgato/translations/it.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Questo dispositivo Elgato Key Light \u00e8 gi\u00e0 configurato.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo Elgato Key Light."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json
index bb7e56211de98b..c17b1dfffee6b3 100644
--- a/homeassistant/components/elgato/translations/no.json
+++ b/homeassistant/components/elgato/translations/no.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Denne Elgato Key Light-enheten er allerede konfigurert.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til Elgato Key Light-enheten."
},
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til Elgato Key Light-enheten."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json
index 263c67a67caee1..ab4cdc4438ee6f 100644
--- a/homeassistant/components/elgato/translations/pl.json
+++ b/homeassistant/components/elgato/translations/pl.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/ru.json b/homeassistant/components/elgato/translations/ru.json
index 7cd7036b861f47..b790d2fa66ba6e 100644
--- a/homeassistant/components/elgato/translations/ru.json
+++ b/homeassistant/components/elgato/translations/ru.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light."
},
"error": {
+ "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 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light."
},
"flow_title": "Elgato Key Light: {serial_number}",
diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json
index 596dc530a2fdf6..2ddd8e6b23e183 100644
--- a/homeassistant/components/elgato/translations/zh-Hant.json
+++ b/homeassistant/components/elgato/translations/zh-Hant.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Elgato Key \u7167\u660e\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"flow_title": "Elgato Key \u7167\u660e\uff1a{serial_number}",
diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json
index 223e2ca3fffd0c..bf0da956d445c1 100644
--- a/homeassistant/components/elkm1/strings.json
+++ b/homeassistant/components/elkm1/strings.json
@@ -15,9 +15,9 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "An ElkM1 with this prefix is already configured",
diff --git a/homeassistant/components/elkm1/translations/ca.json b/homeassistant/components/elkm1/translations/ca.json
index fd69a52d16da8c..ce766c314ed9aa 100644
--- a/homeassistant/components/elkm1/translations/ca.json
+++ b/homeassistant/components/elkm1/translations/ca.json
@@ -5,7 +5,7 @@
"already_configured": "Ja hi ha un Elk-M1 configurat amb aquest prefix"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/elkm1/translations/en.json b/homeassistant/components/elkm1/translations/en.json
index 7fef25d79a6f36..04fd3c189b562f 100644
--- a/homeassistant/components/elkm1/translations/en.json
+++ b/homeassistant/components/elkm1/translations/en.json
@@ -5,7 +5,7 @@
"already_configured": "An ElkM1 with this prefix is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json
index 81265e587b2e41..618299def29208 100644
--- a/homeassistant/components/elkm1/translations/fr.json
+++ b/homeassistant/components/elkm1/translations/fr.json
@@ -16,8 +16,10 @@
"password": "Mot de passe",
"prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).",
"protocol": "Protocole",
+ "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.",
"username": "Nom d'utilisateur"
},
+ "description": "La cha\u00eene d'adresse doit \u00eatre au format \u00abadresse [: port]\u00bb pour \u00abs\u00e9curis\u00e9\u00bb et \u00abnon s\u00e9curis\u00e9\u00bb. Exemple: '192.168.1.1'. Le port est facultatif et vaut par d\u00e9faut 2101 pour \u00abnon s\u00e9curis\u00e9\u00bb et 2601 pour \u00abs\u00e9curis\u00e9\u00bb. Pour le protocole s\u00e9rie, l'adresse doit \u00eatre au format \u00abtty [: baud]\u00bb. Exemple: '/ dev / ttyS1'. Le baud est facultatif et par d\u00e9faut \u00e0 115200.",
"title": "Se connecter a Elk-M1 Control"
}
}
diff --git a/homeassistant/components/elkm1/translations/it.json b/homeassistant/components/elkm1/translations/it.json
index e6eeacaf661b1d..18e53997937f59 100644
--- a/homeassistant/components/elkm1/translations/it.json
+++ b/homeassistant/components/elkm1/translations/it.json
@@ -5,7 +5,7 @@
"already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/elkm1/translations/no.json b/homeassistant/components/elkm1/translations/no.json
index d00c4ab2eef03e..e4b16aa773c1df 100644
--- a/homeassistant/components/elkm1/translations/no.json
+++ b/homeassistant/components/elkm1/translations/no.json
@@ -5,7 +5,7 @@
"already_configured": "En ElkM1 med dette prefikset er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/elkm1/translations/pl.json b/homeassistant/components/elkm1/translations/pl.json
index b38c9aaa6d2cf8..c7f21554dee2e7 100644
--- a/homeassistant/components/elkm1/translations/pl.json
+++ b/homeassistant/components/elkm1/translations/pl.json
@@ -6,8 +6,8 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json
index 94d3a47a8e1aa3..3c84b98b0cade9 100644
--- a/homeassistant/components/elkm1/translations/ru.json
+++ b/homeassistant/components/elkm1/translations/ru.json
@@ -5,7 +5,7 @@
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json
index 01c1197f66a973..0a2f1f60faac0b 100644
--- a/homeassistant/components/elkm1/translations/zh-Hant.json
+++ b/homeassistant/components/elkm1/translations/zh-Hant.json
@@ -5,7 +5,7 @@
"already_configured": "\u4f7f\u7528\u6b64 Prefix \u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/emulated_roku/strings.json b/homeassistant/components/emulated_roku/strings.json
index a47c1c4799b71d..a47f3f9a2e75a9 100644
--- a/homeassistant/components/emulated_roku/strings.json
+++ b/homeassistant/components/emulated_roku/strings.json
@@ -9,7 +9,7 @@
"advertise_port": "Advertise port",
"host_ip": "Host IP",
"listen_port": "Listen port",
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"upnp_bind_multicast": "Bind multicast (True/False)"
},
"title": "Define server configuration"
diff --git a/homeassistant/components/emulated_roku/translations/et.json b/homeassistant/components/emulated_roku/translations/et.json
index b94548b44af47e..cc4dd04a9ea42c 100644
--- a/homeassistant/components/emulated_roku/translations/et.json
+++ b/homeassistant/components/emulated_roku/translations/et.json
@@ -4,7 +4,8 @@
"user": {
"data": {
"host_ip": "",
- "name": "Nimi"
+ "name": "Nimi",
+ "upnp_bind_multicast": "Seo multicast (jah/ei)"
}
}
}
diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py
index 563b2c5195d76f..a086f5ba27c14b 100644
--- a/homeassistant/components/enigma2/media_player.py
+++ b/homeassistant/components/enigma2/media_player.py
@@ -253,17 +253,13 @@ def device_state_attributes(self):
currservice_begin: is in the format '21:00'.
currservice_end: is in the format '21:00'.
"""
- attributes = {}
- if not self.e2_box.in_standby:
- attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = self.e2_box.status_info[
- "isRecording"
- ]
- attributes[ATTR_MEDIA_DESCRIPTION] = self.e2_box.status_info[
+ if self.e2_box.in_standby:
+ return {}
+ return {
+ ATTR_MEDIA_CURRENTLY_RECORDING: self.e2_box.status_info["isRecording"],
+ ATTR_MEDIA_DESCRIPTION: self.e2_box.status_info[
"currservice_fulldescription"
- ]
- attributes[ATTR_MEDIA_START_TIME] = self.e2_box.status_info[
- "currservice_begin"
- ]
- attributes[ATTR_MEDIA_END_TIME] = self.e2_box.status_info["currservice_end"]
-
- return attributes
+ ],
+ ATTR_MEDIA_START_TIME: self.e2_box.status_info["currservice_begin"],
+ ATTR_MEDIA_END_TIME: self.e2_box.status_info["currservice_end"],
+ }
diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json
index 9664031c000559..eb98e1fb2b95f2 100644
--- a/homeassistant/components/enocean/translations/de.json
+++ b/homeassistant/components/enocean/translations/de.json
@@ -4,5 +4,6 @@
"single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"flow_title": "ENOcean-Einrichtung"
- }
+ },
+ "title": "EnOcean"
}
\ No newline at end of file
diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py
index 706308830f6c57..bf35da16fa5f53 100644
--- a/homeassistant/components/environment_canada/camera.py
+++ b/homeassistant/components/environment_canada/camera.py
@@ -86,9 +86,7 @@ def name(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
- attr = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_UPDATED: self.timestamp}
-
- return attr
+ return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_UPDATED: self.timestamp}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py
index 4813fd47a9246a..873d9935ee8443 100644
--- a/homeassistant/components/envirophat/sensor.py
+++ b/homeassistant/components/envirophat/sensor.py
@@ -6,7 +6,13 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS, VOLT
+from homeassistant.const import (
+ CONF_DISPLAY_OPTIONS,
+ CONF_NAME,
+ PRESSURE_HPA,
+ TEMP_CELSIUS,
+ VOLT,
+)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -30,7 +36,7 @@
"magnetometer_y": ["magnetometer_y", " ", "mdi:magnet"],
"magnetometer_z": ["magnetometer_z", " ", "mdi:magnet"],
"temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer"],
- "pressure": ["pressure", "hPa", "mdi:gauge"],
+ "pressure": ["pressure", PRESSURE_HPA, "mdi:gauge"],
"voltage_0": ["voltage_0", VOLT, "mdi:flash"],
"voltage_1": ["voltage_1", VOLT, "mdi:flash"],
"voltage_2": ["voltage_2", VOLT, "mdi:flash"],
diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py
index df0dcc536b5b35..235caa8d1a393e 100644
--- a/homeassistant/components/epson/media_player.py
+++ b/homeassistant/components/epson/media_player.py
@@ -235,7 +235,6 @@ async def async_media_previous_track(self):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
- attributes = {}
- if self._cmode is not None:
- attributes[ATTR_CMODE] = self._cmode
- return attributes
+ if self._cmode is None:
+ return {}
+ return {ATTR_CMODE: self._cmode}
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index f1b22c13bf13ce..c9d07a22ec6c83 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -66,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
password = entry.data[CONF_PASSWORD]
+ device_id = None
zeroconf_instance = await zeroconf.async_get_instance(hass)
@@ -129,6 +130,15 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None:
"Can only generate events under esphome domain! (%s)", host
)
return
+
+ # Call native tag scan
+ if service_name == "tag_scanned":
+ tag_id = service_data["tag_id"]
+ hass.async_create_task(
+ hass.components.tag.async_scan_tag(tag_id, device_id)
+ )
+ return
+
hass.bus.async_fire(service.service, service_data)
else:
hass.async_create_task(
@@ -166,10 +176,13 @@ def async_on_state_subscription(entity_id: str) -> None:
async def on_login() -> None:
"""Subscribe to states and list entities on successful API login."""
+ nonlocal device_id
try:
entry_data.device_info = await cli.device_info()
entry_data.available = True
- await _async_setup_device_registry(hass, entry, entry_data.device_info)
+ device_id = await _async_setup_device_registry(
+ hass, entry, entry_data.device_info
+ )
entry_data.async_update_device_state(hass)
entity_infos, services = await cli.list_entities_services()
@@ -265,7 +278,7 @@ async def _async_setup_device_registry(
if device_info.compilation_time:
sw_version += f" ({device_info.compilation_time})"
device_registry = await dr.async_get_registry(hass)
- device_registry.async_get_or_create(
+ entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
name=device_info.name,
@@ -273,6 +286,7 @@ async def _async_setup_device_registry(
model=device_info.model,
sw_version=sw_version,
)
+ return entry.id
async def _register_service(
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index c57ff4a55207cf..123c7931e41d34 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -6,7 +6,5 @@
"requirements": ["aioesphomeapi==2.6.3"],
"zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter"],
- "after_dependencies": [
- "zeroconf"
- ]
+ "after_dependencies": ["zeroconf", "tag"]
}
diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json
index af7515245b58e0..c0f6aafa1b2e38 100644
--- a/homeassistant/components/esphome/strings.json
+++ b/homeassistant/components/esphome/strings.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP is already configured",
- "already_in_progress": "ESP configuration is already in progress"
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
},
"error": {
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips",
@@ -30,4 +30,4 @@
},
"flow_title": "ESPHome: {name}"
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json
index 8833749d071db3..ff16b40bc25bc9 100644
--- a/homeassistant/components/esphome/translations/ca.json
+++ b/homeassistant/components/esphome/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP ja est\u00e0 configurat",
- "already_in_progress": "La configuraci\u00f3 de l'ESP ja est\u00e0 en curs"
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs"
},
"error": {
"connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.",
diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json
index b437d6d79fd281..4d3691e0cbc5d5 100644
--- a/homeassistant/components/esphome/translations/cs.json
+++ b/homeassistant/components/esphome/translations/cs.json
@@ -1,8 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Tento ESP uzel je ji\u017e nakonfigurov\u00e1n",
- "already_in_progress": "Konfigurace uzlu ESP ji\u017e prob\u00edh\u00e1"
+ "already_configured": "Tento ESP uzel je ji\u017e nakonfigurov\u00e1n"
},
"error": {
"connection_error": "Nelze se p\u0159ipojit k ESP. Zkontrolujte, zda va\u0161e YAML konfigurace obsahuje \u0159\u00e1dek 'api:'.",
diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json
index 6386e1f70930bc..b31664cc60401c 100644
--- a/homeassistant/components/esphome/translations/en.json
+++ b/homeassistant/components/esphome/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP is already configured",
- "already_in_progress": "ESP configuration is already in progress"
+ "already_in_progress": "Configuration flow is already in progress"
},
"error": {
"connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json
index 19ff26493a8a21..feef1bda549684 100644
--- a/homeassistant/components/esphome/translations/it.json
+++ b/homeassistant/components/esphome/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP \u00e8 gi\u00e0 configurato",
- "already_in_progress": "La configurazione ESP \u00e8 gi\u00e0 in corso"
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso"
},
"error": {
"connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".",
diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json
index 3c2dafff34d48f..cc775d05a979a8 100644
--- a/homeassistant/components/esphome/translations/no.json
+++ b/homeassistant/components/esphome/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP er allerede konfigurert",
- "already_in_progress": "ESP-konfigurasjon p\u00e5g\u00e5r allerede"
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede"
},
"error": {
"connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.",
diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json
index c03e4619ec92c2..332b4176f48716 100644
--- a/homeassistant/components/esphome/translations/ru.json
+++ b/homeassistant/components/esphome/translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f."
+ "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."
},
"error": {
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.",
diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json
index be25ddd6366364..ce4686508754e0 100644
--- a/homeassistant/components/esphome/translations/zh-Hant.json
+++ b/homeassistant/components/esphome/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ESP \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "ESP \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002"
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d"
},
"error": {
"connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002",
diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/fan/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/fan/translations/et.json b/homeassistant/components/fan/translations/et.json
index 6652568a0a7b24..2b141351e1ded6 100644
--- a/homeassistant/components/fan/translations/et.json
+++ b/homeassistant/components/fan/translations/et.json
@@ -1,4 +1,18 @@
{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "L\u00fclita {entity_name} v\u00e4lja",
+ "turn_on": "L\u00fclita {entity_name} sisse"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
diff --git a/homeassistant/components/fan/translations/uk.json b/homeassistant/components/fan/translations/uk.json
index 80b64c28c2f9ce..3fd103cd244c56 100644
--- a/homeassistant/components/fan/translations/uk.json
+++ b/homeassistant/components/fan/translations/uk.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py
index f109103a99c9b3..ca752208f1258f 100644
--- a/homeassistant/components/ffmpeg/__init__.py
+++ b/homeassistant/components/ffmpeg/__init__.py
@@ -7,6 +7,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONTENT_TYPE_MULTIPART,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
@@ -122,9 +123,9 @@ async def async_get_version(self):
def ffmpeg_stream_content_type(self):
"""Return HTTP content type for ffmpeg stream."""
if self._major_version is not None and self._major_version > 3:
- return "multipart/x-mixed-replace;boundary=ffmpeg"
+ return CONTENT_TYPE_MULTIPART.format("ffmpeg")
- return "multipart/x-mixed-replace;boundary=ffserver"
+ return CONTENT_TYPE_MULTIPART.format("ffserver")
class FFmpegBase(Entity):
diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py
index a8842f9c40183c..9b4218c011cffb 100644
--- a/homeassistant/components/ffmpeg_motion/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py
@@ -4,7 +4,11 @@
import haffmpeg.sensor as ffmpeg_sensor
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.components.ffmpeg import (
CONF_EXTRA_ARGUMENTS,
CONF_INITIAL_STATE,
@@ -119,4 +123,4 @@ async def _async_start_ffmpeg(self, entity_ids):
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
- return "motion"
+ return DEVICE_CLASS_MOTION
diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py
index 6ada2bb274896d..387f25afe6e1db 100644
--- a/homeassistant/components/ffmpeg_noise/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py
@@ -4,7 +4,7 @@
import haffmpeg.sensor as ffmpeg_sensor
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
+from homeassistant.components.binary_sensor import DEVICE_CLASS_SOUND, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import (
CONF_EXTRA_ARGUMENTS,
CONF_INITIAL_STATE,
@@ -84,4 +84,4 @@ async def _async_start_ffmpeg(self, entity_ids):
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
- return "sound"
+ return DEVICE_CLASS_SOUND
diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py
index 251bd1df6a3ed6..21ce22ea8e31c1 100644
--- a/homeassistant/components/fibaro/binary_sensor.py
+++ b/homeassistant/components/fibaro/binary_sensor.py
@@ -1,7 +1,14 @@
"""Support for Fibaro binary sensors."""
import logging
-from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_SMOKE,
+ DEVICE_CLASS_WINDOW,
+ DOMAIN,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON
from . import FIBARO_DEVICES, FibaroDevice
@@ -10,11 +17,11 @@
SENSOR_TYPES = {
"com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"],
- "com.fibaro.motionSensor": ["Motion", "mdi:run", "motion"],
- "com.fibaro.doorSensor": ["Door", "mdi:window-open", "door"],
- "com.fibaro.windowSensor": ["Window", "mdi:window-open", "window"],
- "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", "smoke"],
- "com.fibaro.FGMS001": ["Motion", "mdi:run", "motion"],
+ "com.fibaro.motionSensor": ["Motion", "mdi:run", DEVICE_CLASS_MOTION],
+ "com.fibaro.doorSensor": ["Door", "mdi:window-open", DEVICE_CLASS_DOOR],
+ "com.fibaro.windowSensor": ["Window", "mdi:window-open", DEVICE_CLASS_WINDOW],
+ "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", DEVICE_CLASS_SMOKE],
+ "com.fibaro.FGMS001": ["Motion", "mdi:run", DEVICE_CLASS_MOTION],
"com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"],
}
diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py
index e9e7265f917bfe..3c8129700227e4 100644
--- a/homeassistant/components/fibaro/sensor.py
+++ b/homeassistant/components/fibaro/sensor.py
@@ -7,6 +7,7 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -35,7 +36,7 @@
None,
DEVICE_CLASS_HUMIDITY,
],
- "com.fibaro.lightSensor": ["Light", "lx", None, DEVICE_CLASS_ILLUMINANCE],
+ "com.fibaro.lightSensor": ["Light", LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE],
}
_LOGGER = logging.getLogger(__name__)
@@ -71,7 +72,7 @@ def __init__(self, fibaro_device):
try:
if not self._unit:
if self.fibaro_device.properties.unit == "lux":
- self._unit = "lx"
+ self._unit = LIGHT_LUX
elif self.fibaro_device.properties.unit == "C":
self._unit = TEMP_CELSIUS
elif self.fibaro_device.properties.unit == "F":
diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py
index 63042ec629228c..27122a3cb9c568 100644
--- a/homeassistant/components/filesize/sensor.py
+++ b/homeassistant/components/filesize/sensor.py
@@ -78,12 +78,11 @@ def icon(self):
@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
- attr = {
+ return {
"path": self._path,
"last_updated": self._last_updated,
"bytes": self._size,
}
- return attr
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py
index b0cc08bc94502c..72f4b00c4e098e 100644
--- a/homeassistant/components/filter/sensor.py
+++ b/homeassistant/components/filter/sensor.py
@@ -319,8 +319,7 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- state_attr = {ATTR_ENTITY_ID: self._entity}
- return state_attr
+ return {ATTR_ENTITY_ID: self._entity}
class FilterState:
diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py
index d81f353c222f8c..6cd62333a8714b 100644
--- a/homeassistant/components/fints/sensor.py
+++ b/homeassistant/components/fints/sensor.py
@@ -169,14 +169,6 @@ def __init__(self, client: FinTsClient, account, name: str) -> None:
self._balance: float = None
self._currency: str = None
- @property
- def should_poll(self) -> bool:
- """Return True.
-
- Data needs to be polled from the bank servers.
- """
- return True
-
def update(self) -> None:
"""Get the current balance and currency for the account."""
bank = self._client.client
@@ -229,14 +221,6 @@ def __init__(self, client: FinTsClient, account, name: str) -> None:
self._holdings = []
self._total: float = None
- @property
- def should_poll(self) -> bool:
- """Return True.
-
- Data needs to be polled from the bank servers.
- """
- return True
-
def update(self) -> None:
"""Get the current holdings for the account."""
bank = self._client.client
diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py
index b64a88cbf5730a..c0394a95a49ac0 100644
--- a/homeassistant/components/firmata/__init__.py
+++ b/homeassistant/components/firmata/__init__.py
@@ -6,7 +6,17 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ CONF_BINARY_SENSORS,
+ CONF_LIGHTS,
+ CONF_MAXIMUM,
+ CONF_MINIMUM,
+ CONF_NAME,
+ CONF_PIN,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+ EVENT_HOMEASSISTANT_STOP,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -14,21 +24,22 @@
from .const import (
CONF_ARDUINO_INSTANCE_ID,
CONF_ARDUINO_WAIT,
- CONF_BINARY_SENSORS,
+ CONF_DIFFERENTIAL,
CONF_INITIAL_STATE,
CONF_NEGATE_STATE,
- CONF_PIN,
CONF_PIN_MODE,
+ CONF_PLATFORM_MAP,
CONF_SAMPLING_INTERVAL,
CONF_SERIAL_BAUD_RATE,
CONF_SERIAL_PORT,
CONF_SLEEP_TUNE,
- CONF_SWITCHES,
DOMAIN,
FIRMATA_MANUFACTURER,
+ PIN_MODE_ANALOG,
PIN_MODE_INPUT,
PIN_MODE_OUTPUT,
PIN_MODE_PULLUP,
+ PIN_MODE_PWM,
)
_LOGGER = logging.getLogger(__name__)
@@ -40,8 +51,8 @@
SWITCH_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
+ # Both digital and analog pins may be used as digital output
vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
- # will be analog mode in future too
vol.Required(CONF_PIN_MODE): PIN_MODE_OUTPUT,
vol.Optional(CONF_INITIAL_STATE, default=False): cv.boolean,
vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean,
@@ -49,17 +60,45 @@
required=True,
)
+LIGHT_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ # Both digital and analog pins may be used as PWM/analog output
+ vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
+ vol.Required(CONF_PIN_MODE): PIN_MODE_PWM,
+ vol.Optional(CONF_INITIAL_STATE, default=0): cv.positive_int,
+ vol.Optional(CONF_MINIMUM, default=0): cv.positive_int,
+ vol.Optional(CONF_MAXIMUM, default=255): cv.positive_int,
+ },
+ required=True,
+)
+
BINARY_SENSOR_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
+ # Both digital and analog pins may be used as digital input
vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
- # will be analog mode in future too
vol.Required(CONF_PIN_MODE): vol.Any(PIN_MODE_INPUT, PIN_MODE_PULLUP),
vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean,
},
required=True,
)
+SENSOR_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ # Currently only analog input sensor is implemented
+ vol.Required(CONF_PIN): ANALOG_PIN_SCHEMA,
+ vol.Required(CONF_PIN_MODE): PIN_MODE_ANALOG,
+ # Default differential is 40 to avoid a flood of messages on initial setup
+ # in case pin is unplugged. Firmata responds really really fast
+ vol.Optional(CONF_DIFFERENTIAL, default=40): vol.All(
+ cv.positive_int, vol.Range(min=1)
+ ),
+ },
+ required=True,
+)
+
BOARD_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_SERIAL_PORT): cv.string,
@@ -71,7 +110,9 @@
),
vol.Optional(CONF_SAMPLING_INTERVAL): cv.positive_int,
vol.Optional(CONF_SWITCHES): [SWITCH_SCHEMA],
+ vol.Optional(CONF_LIGHTS): [LIGHT_SCHEMA],
vol.Optional(CONF_BINARY_SENSORS): [BINARY_SENSOR_SCHEMA],
+ vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA],
},
required=True,
)
@@ -155,14 +196,11 @@ async def handle_shutdown(event) -> None:
sw_version=board.firmware_version,
)
- if CONF_BINARY_SENSORS in config_entry.data:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
- )
- if CONF_SWITCHES in config_entry.data:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, "switch")
- )
+ for (conf, platform) in CONF_PLATFORM_MAP.items():
+ if conf in config_entry.data:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
return True
@@ -173,16 +211,11 @@ async def async_unload_entry(
_LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME])
unload_entries = []
- if CONF_BINARY_SENSORS in config_entry.data:
- unload_entries.append(
- hass.config_entries.async_forward_entry_unload(
- config_entry, "binary_sensor"
+ for (conf, platform) in CONF_PLATFORM_MAP.items():
+ if conf in config_entry.data:
+ unload_entries.append(
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
)
- )
- if CONF_SWITCHES in config_entry.data:
- unload_entries.append(
- hass.config_entries.async_forward_entry_unload(config_entry, "switch")
- )
results = []
if unload_entries:
results = await asyncio.gather(*unload_entries)
diff --git a/homeassistant/components/firmata/binary_sensor.py b/homeassistant/components/firmata/binary_sensor.py
index 4576b8dc69eff7..c2708fc27533da 100644
--- a/homeassistant/components/firmata/binary_sensor.py
+++ b/homeassistant/components/firmata/binary_sensor.py
@@ -4,10 +4,10 @@
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
-from .const import CONF_NEGATE_STATE, CONF_PIN, CONF_PIN_MODE, DOMAIN
+from .const import CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataBinaryDigitalInput, FirmataPinUsedException
@@ -30,7 +30,7 @@ async def async_setup_entry(
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
- "Could not setup binary sensor on pin %s since pin already in use.",
+ "Could not setup binary sensor on pin %s since pin already in use",
binary_sensor[CONF_PIN],
)
continue
diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py
index bae30014d63236..73e3c004cb91ef 100644
--- a/homeassistant/components/firmata/board.py
+++ b/homeassistant/components/firmata/board.py
@@ -5,17 +5,23 @@
from pymata_express.pymata_express import PymataExpress
from pymata_express.pymata_express_serial import serial
-from homeassistant.const import CONF_NAME
+from homeassistant.const import (
+ CONF_BINARY_SENSORS,
+ CONF_LIGHTS,
+ CONF_NAME,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+)
from .const import (
CONF_ARDUINO_INSTANCE_ID,
CONF_ARDUINO_WAIT,
- CONF_BINARY_SENSORS,
CONF_SAMPLING_INTERVAL,
CONF_SERIAL_BAUD_RATE,
CONF_SERIAL_PORT,
CONF_SLEEP_TUNE,
- CONF_SWITCHES,
+ PIN_TYPE_ANALOG,
+ PIN_TYPE_DIGITAL,
)
_LOGGER = logging.getLogger(__name__)
@@ -34,13 +40,19 @@ def __init__(self, config: dict):
self.protocol_version = None
self.name = self.config[CONF_NAME]
self.switches = []
+ self.lights = []
self.binary_sensors = []
+ self.sensors = []
self.used_pins = []
if CONF_SWITCHES in self.config:
self.switches = self.config[CONF_SWITCHES]
+ if CONF_LIGHTS in self.config:
+ self.lights = self.config[CONF_LIGHTS]
if CONF_BINARY_SENSORS in self.config:
self.binary_sensors = self.config[CONF_BINARY_SENSORS]
+ if CONF_SENSORS in self.config:
+ self.sensors = self.config[CONF_SENSORS]
async def async_setup(self, tries=0) -> bool:
"""Set up a Firmata instance."""
@@ -109,11 +121,11 @@ def mark_pin_used(self, pin: FirmataPinType) -> bool:
def get_pin_type(self, pin: FirmataPinType) -> tuple:
"""Return the type and Firmata location of a pin on the board."""
if isinstance(pin, str):
- pin_type = "analog"
+ pin_type = PIN_TYPE_ANALOG
firmata_pin = int(pin[1:])
firmata_pin += self.api.first_analog_pin
else:
- pin_type = "digital"
+ pin_type = PIN_TYPE_DIGITAL
firmata_pin = pin
return (pin_type, firmata_pin)
diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py
index 1ad3cbb8423afa..6259582b5f790b 100644
--- a/homeassistant/components/firmata/const.py
+++ b/homeassistant/components/firmata/const.py
@@ -1,24 +1,35 @@
"""Constants for the Firmata component."""
-import logging
-
-LOGGER = logging.getLogger(__package__)
+from homeassistant.const import (
+ CONF_BINARY_SENSORS,
+ CONF_LIGHTS,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+)
CONF_ARDUINO_INSTANCE_ID = "arduino_instance_id"
CONF_ARDUINO_WAIT = "arduino_wait"
-CONF_BINARY_SENSORS = "binary_sensors"
+CONF_DIFFERENTIAL = "differential"
CONF_INITIAL_STATE = "initial"
CONF_NAME = "name"
CONF_NEGATE_STATE = "negate"
-CONF_PIN = "pin"
CONF_PINS = "pins"
CONF_PIN_MODE = "pin_mode"
+PIN_MODE_ANALOG = "ANALOG"
PIN_MODE_OUTPUT = "OUTPUT"
+PIN_MODE_PWM = "PWM"
PIN_MODE_INPUT = "INPUT"
PIN_MODE_PULLUP = "PULLUP"
+PIN_TYPE_ANALOG = 1
+PIN_TYPE_DIGITAL = 0
CONF_SAMPLING_INTERVAL = "sampling_interval"
CONF_SERIAL_BAUD_RATE = "serial_baud_rate"
CONF_SERIAL_PORT = "serial_port"
CONF_SLEEP_TUNE = "sleep_tune"
-CONF_SWITCHES = "switches"
DOMAIN = "firmata"
FIRMATA_MANUFACTURER = "Firmata"
+CONF_PLATFORM_MAP = {
+ CONF_BINARY_SENSORS: "binary_sensor",
+ CONF_LIGHTS: "light",
+ CONF_SENSORS: "sensor",
+ CONF_SWITCHES: "switch",
+}
diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py
new file mode 100644
index 00000000000000..e95b51014136b0
--- /dev/null
+++ b/homeassistant/components/firmata/light.py
@@ -0,0 +1,98 @@
+"""Support for Firmata light output."""
+
+import logging
+from typing import Type
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ SUPPORT_BRIGHTNESS,
+ LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME, CONF_PIN
+from homeassistant.core import HomeAssistant
+
+from .board import FirmataPinType
+from .const import CONF_INITIAL_STATE, CONF_PIN_MODE, DOMAIN
+from .entity import FirmataPinEntity
+from .pin import FirmataBoardPin, FirmataPinUsedException, FirmataPWMOutput
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Firmata lights."""
+ new_entities = []
+
+ board = hass.data[DOMAIN][config_entry.entry_id]
+ for light in board.lights:
+ pin = light[CONF_PIN]
+ pin_mode = light[CONF_PIN_MODE]
+ initial = light[CONF_INITIAL_STATE]
+ minimum = light[CONF_MINIMUM]
+ maximum = light[CONF_MAXIMUM]
+ api = FirmataPWMOutput(board, pin, pin_mode, initial, minimum, maximum)
+ try:
+ api.setup()
+ except FirmataPinUsedException:
+ _LOGGER.error(
+ "Could not setup light on pin %s since pin already in use",
+ light[CONF_PIN],
+ )
+ continue
+ name = light[CONF_NAME]
+ light_entity = FirmataLight(api, config_entry, name, pin)
+ new_entities.append(light_entity)
+
+ if new_entities:
+ async_add_entities(new_entities)
+
+
+class FirmataLight(FirmataPinEntity, LightEntity):
+ """Representation of a light on a Firmata board."""
+
+ def __init__(
+ self,
+ api: Type[FirmataBoardPin],
+ config_entry: ConfigEntry,
+ name: str,
+ pin: FirmataPinType,
+ ):
+ """Initialize the light pin entity."""
+ super().__init__(api, config_entry, name, pin)
+
+ # Default first turn on to max
+ self._last_on_level = 255
+
+ async def async_added_to_hass(self) -> None:
+ """Set up a light."""
+ await self._api.start_pin()
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if light is on."""
+ return self._api.state > 0
+
+ @property
+ def brightness(self) -> int:
+ """Return the brightness of the light."""
+ return self._api.state
+
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ return SUPPORT_BRIGHTNESS
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn on light."""
+ level = kwargs.get(ATTR_BRIGHTNESS, self._last_on_level)
+ await self._api.set_level(level)
+ self.async_write_ha_state()
+ self._last_on_level = level
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn off light."""
+ await self._api.set_level(0)
+ self.async_write_ha_state()
diff --git a/homeassistant/components/firmata/manifest.json b/homeassistant/components/firmata/manifest.json
index d894c0a440b8fd..8b283c4f81df99 100644
--- a/homeassistant/components/firmata/manifest.json
+++ b/homeassistant/components/firmata/manifest.json
@@ -4,7 +4,7 @@
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/firmata",
"requirements": [
- "pymata-express==1.13"
+ "pymata-express==1.19"
],
"codeowners": [
"@DaAwesomeP"
diff --git a/homeassistant/components/firmata/pin.py b/homeassistant/components/firmata/pin.py
index 644986fb66cea3..3259d76cbb3ffb 100644
--- a/homeassistant/components/firmata/pin.py
+++ b/homeassistant/components/firmata/pin.py
@@ -2,10 +2,8 @@
import logging
from typing import Callable
-from homeassistant.core import callback
-
from .board import FirmataBoard, FirmataPinType
-from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP
+from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP, PIN_TYPE_ANALOG
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +23,10 @@ def __init__(self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str):
self._pin_type, self._firmata_pin = self.board.get_pin_type(self._pin)
self._state = None
+ if self._pin_type == PIN_TYPE_ANALOG:
+ # Pymata wants the analog pin formatted as the # from "A#"
+ self._analog_pin = int(self._pin[1:])
+
def setup(self):
"""Set up a pin and make sure it is valid."""
if not self.board.mark_pin_used(self._pin):
@@ -85,6 +87,53 @@ async def turn_off(self) -> None:
self._state = False
+class FirmataPWMOutput(FirmataBoardPin):
+ """Representation of a Firmata PWM/analog Output Pin."""
+
+ def __init__(
+ self,
+ board: FirmataBoard,
+ pin: FirmataPinType,
+ pin_mode: str,
+ initial: bool,
+ minimum: int,
+ maximum: int,
+ ):
+ """Initialize the PWM/analog output pin."""
+ self._initial = initial
+ self._min = minimum
+ self._max = maximum
+ self._range = self._max - self._min
+ super().__init__(board, pin, pin_mode)
+
+ async def start_pin(self) -> None:
+ """Set initial state on a pin."""
+ _LOGGER.debug(
+ "Setting initial state for PWM/analog output pin %s on board %s to %d",
+ self._pin,
+ self.board.name,
+ self._initial,
+ )
+ api = self.board.api
+ await api.set_pin_mode_pwm_output(self._firmata_pin)
+
+ new_pin_state = round((self._initial * self._range) / 255) + self._min
+ await api.pwm_write(self._firmata_pin, new_pin_state)
+ self._state = self._initial
+
+ @property
+ def state(self) -> int:
+ """Return PWM/analog state."""
+ return self._state
+
+ async def set_level(self, level: int) -> None:
+ """Set PWM/analog output."""
+ _LOGGER.debug("Setting PWM/analog output on pin %s to %d", self._pin, level)
+ new_pin_state = round((level * self._range) / 255) + self._min
+ await self.board.api.pwm_write(self._firmata_pin, new_pin_state)
+ self._state = level
+
+
class FirmataBinaryDigitalInput(FirmataBoardPin):
"""Representation of a Firmata Digital Input Pin."""
@@ -99,7 +148,7 @@ def __init__(
async def start_pin(self, forward_callback: Callable[[], None]) -> None:
"""Get initial state and start reporting a pin."""
_LOGGER.debug(
- "Starting reporting updates for input pin %s on board %s",
+ "Starting reporting updates for digital input pin %s on board %s",
self._pin,
self.board.name,
)
@@ -133,7 +182,6 @@ def is_on(self) -> bool:
"""Return true if digital input is on."""
return self._state
- @callback
async def latch_callback(self, data: list) -> None:
"""Update pin state on callback."""
if data[1] != self._firmata_pin:
@@ -151,3 +199,65 @@ async def latch_callback(self, data: list) -> None:
return
self._state = new_state
self._forward_callback()
+
+
+class FirmataAnalogInput(FirmataBoardPin):
+ """Representation of a Firmata Analog Input Pin."""
+
+ def __init__(
+ self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, differential: int
+ ):
+ """Initialize the analog input pin."""
+ self._differential = differential
+ self._forward_callback = None
+ super().__init__(board, pin, pin_mode)
+
+ async def start_pin(self, forward_callback: Callable[[], None]) -> None:
+ """Get initial state and start reporting a pin."""
+ _LOGGER.debug(
+ "Starting reporting updates for analog input pin %s on board %s",
+ self._pin,
+ self.board.name,
+ )
+ self._forward_callback = forward_callback
+ api = self.board.api
+ # Only PIN_MODE_ANALOG_INPUT mode is supported as sensor input
+ await api.set_pin_mode_analog_input(
+ self._analog_pin, self.latch_callback, self._differential
+ )
+
+ self._state = (await self.board.api.analog_read(self._analog_pin))[0]
+
+ self._forward_callback()
+
+ async def stop_pin(self) -> None:
+ """Stop reporting analog input pin."""
+ _LOGGER.debug(
+ "Stopping reporting updates for analog input pin %s on board %s",
+ self._pin,
+ self.board.name,
+ )
+ api = self.board.api
+ await api.disable_analog_reporting(self._analog_pin)
+
+ @property
+ def state(self) -> int:
+ """Return sensor state."""
+ return self._state
+
+ async def latch_callback(self, data: list) -> None:
+ """Update pin state on callback."""
+ if data[1] != self._analog_pin:
+ return
+ _LOGGER.debug(
+ "Received latch %d for analog input pin %s on board %s",
+ data[2],
+ self._pin,
+ self.board.name,
+ )
+ new_state = data[2]
+ if self._state == new_state:
+ _LOGGER.debug("stopping")
+ return
+ self._state = new_state
+ self._forward_callback()
diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py
new file mode 100644
index 00000000000000..cb9db1f11e5f55
--- /dev/null
+++ b/homeassistant/components/firmata/sensor.py
@@ -0,0 +1,59 @@
+"""Support for Firmata sensor input."""
+
+import logging
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_NAME, CONF_PIN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+
+from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN
+from .entity import FirmataPinEntity
+from .pin import FirmataAnalogInput, FirmataPinUsedException
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Firmata sensors."""
+ new_entities = []
+
+ board = hass.data[DOMAIN][config_entry.entry_id]
+ for sensor in board.sensors:
+ pin = sensor[CONF_PIN]
+ pin_mode = sensor[CONF_PIN_MODE]
+ differential = sensor[CONF_DIFFERENTIAL]
+ api = FirmataAnalogInput(board, pin, pin_mode, differential)
+ try:
+ api.setup()
+ except FirmataPinUsedException:
+ _LOGGER.error(
+ "Could not setup sensor on pin %s since pin already in use",
+ sensor[CONF_PIN],
+ )
+ continue
+ name = sensor[CONF_NAME]
+ sensor_entity = FirmataSensor(api, config_entry, name, pin)
+ new_entities.append(sensor_entity)
+
+ if new_entities:
+ async_add_entities(new_entities)
+
+
+class FirmataSensor(FirmataPinEntity, Entity):
+ """Representation of a sensor on a Firmata board."""
+
+ async def async_added_to_hass(self) -> None:
+ """Set up a sensor."""
+ await self._api.start_pin(self.async_write_ha_state)
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Stop reporting a sensor."""
+ await self._api.stop_pin()
+
+ @property
+ def state(self) -> int:
+ """Return sensor state."""
+ return self._api.state
diff --git a/homeassistant/components/firmata/strings.json b/homeassistant/components/firmata/strings.json
index 68d7ae8c041a97..90e62325c2ab13 100644
--- a/homeassistant/components/firmata/strings.json
+++ b/homeassistant/components/firmata/strings.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Cannot connect to Firmata board during setup"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {}
}
diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py
index ab67a6d6840101..f1aaf3357c06d9 100644
--- a/homeassistant/components/firmata/switch.py
+++ b/homeassistant/components/firmata/switch.py
@@ -4,16 +4,10 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
-from .const import (
- CONF_INITIAL_STATE,
- CONF_NEGATE_STATE,
- CONF_PIN,
- CONF_PIN_MODE,
- DOMAIN,
-)
+from .const import CONF_INITIAL_STATE, CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataBinaryDigitalOutput, FirmataPinUsedException
@@ -37,7 +31,7 @@ async def async_setup_entry(
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
- "Could not setup switch on pin %s since pin already in use.",
+ "Could not setup switch on pin %s since pin already in use",
switch[CONF_PIN],
)
continue
@@ -55,7 +49,6 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity):
async def async_added_to_hass(self) -> None:
"""Set up a switch."""
await self._api.start_pin()
- self.async_write_ha_state()
@property
def is_on(self) -> bool:
@@ -64,12 +57,10 @@ def is_on(self) -> bool:
async def async_turn_on(self, **kwargs) -> None:
"""Turn on switch."""
- _LOGGER.debug("Turning switch %s on", self._name)
await self._api.turn_on()
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
"""Turn off switch."""
- _LOGGER.debug("Turning switch %s off", self._name)
await self._api.turn_off()
self.async_write_ha_state()
diff --git a/homeassistant/components/firmata/translations/ca.json b/homeassistant/components/firmata/translations/ca.json
index 29a04d50b3e5fb..cf210681e533f0 100644
--- a/homeassistant/components/firmata/translations/ca.json
+++ b/homeassistant/components/firmata/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "No s'ha pogut connectar a la placa Frimata durant la configuraci\u00f3"
+ "cannot_connect": "Ha fallat la connexi\u00f3"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/en.json b/homeassistant/components/firmata/translations/en.json
index 39ea716d9755a9..03668c055d8010 100644
--- a/homeassistant/components/firmata/translations/en.json
+++ b/homeassistant/components/firmata/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Cannot connect to Firmata board during setup"
+ "cannot_connect": "Failed to connect"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/fr.json b/homeassistant/components/firmata/translations/fr.json
new file mode 100644
index 00000000000000..a66d58dce876d5
--- /dev/null
+++ b/homeassistant/components/firmata/translations/fr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Impossible de se connecter \u00e0 la carte Firmata pendant la configuration"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/it.json b/homeassistant/components/firmata/translations/it.json
index 79c4a093140a9e..6c2460ab0b2ad8 100644
--- a/homeassistant/components/firmata/translations/it.json
+++ b/homeassistant/components/firmata/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Impossibile connettersi alla scheda Firmata durante la configurazione"
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"one": "uno",
diff --git a/homeassistant/components/firmata/translations/no.json b/homeassistant/components/firmata/translations/no.json
index e1e5c8f1ea4d95..9edce0bbb15dda 100644
--- a/homeassistant/components/firmata/translations/no.json
+++ b/homeassistant/components/firmata/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Kan ikke koble til Firmata Board under installasjonen"
+ "cannot_connect": "Tilkobling mislyktes."
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/ru.json b/homeassistant/components/firmata/translations/ru.json
index 64737774a2d735..6bea2885a8e84e 100644
--- a/homeassistant/components/firmata/translations/ru.json
+++ b/homeassistant/components/firmata/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043b\u0430\u0442\u0435 Firmata \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/zh-Hant.json b/homeassistant/components/firmata/translations/zh-Hant.json
index d86ad56653cda7..d587eaf5ab2a50 100644
--- a/homeassistant/components/firmata/translations/zh-Hant.json
+++ b/homeassistant/components/firmata/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "\u65bc\u8a2d\u5b9a\u671f\u9593\uff0c\u7121\u6cd5\u9023\u7dda\u81f3 Firmata \u677f"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index f0914ab35f0003..f6e3fd90fe5c35 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -185,9 +185,7 @@ def fitbit_configuration_callback(callback_data):
else:
setup_platform(hass, config, add_entities, discovery_info)
- start_url = (
- f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
- )
+ start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
description = f"""Please create a Fitbit developer app at
https://dev.fitbit.com/apps/new.
@@ -222,7 +220,7 @@ def request_oauth_completion(hass):
def fitbit_configuration_callback(callback_data):
"""Handle configuration updates."""
- start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}"
+ start_url = f"{get_url(hass)}{FITBIT_AUTH_START}"
description = f"Please authorize Fitbit by visiting {start_url}"
@@ -314,9 +312,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET)
)
- redirect_uri = (
- f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
- )
+ redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
fitbit_auth_start_url, _ = oauth.authorize_token_url(
redirect_uri=redirect_uri,
diff --git a/homeassistant/components/flick_electric/strings.json b/homeassistant/components/flick_electric/strings.json
index d3d1a9d3bdfc70..a37a803fb6a7c1 100644
--- a/homeassistant/components/flick_electric/strings.json
+++ b/homeassistant/components/flick_electric/strings.json
@@ -13,12 +13,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "That account is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/flick_electric/translations/ca.json b/homeassistant/components/flick_electric/translations/ca.json
index f4eb1bffd459c2..5bc9684d3b2862 100644
--- a/homeassistant/components/flick_electric/translations/ca.json
+++ b/homeassistant/components/flick_electric/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquest compte ja est\u00e0 configurat"
+ "already_configured": "El compte ja ha estat configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json
index b69e8de8f7c240..d63283fe36b076 100644
--- a/homeassistant/components/flick_electric/translations/de.json
+++ b/homeassistant/components/flick_electric/translations/de.json
@@ -16,5 +16,6 @@
}
}
}
- }
+ },
+ "title": "Flick Electric"
}
\ No newline at end of file
diff --git a/homeassistant/components/flick_electric/translations/en.json b/homeassistant/components/flick_electric/translations/en.json
index eb1ce7f296d902..4a3e4c8fbe6536 100644
--- a/homeassistant/components/flick_electric/translations/en.json
+++ b/homeassistant/components/flick_electric/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "That account is already configured"
+ "already_configured": "Account is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/flick_electric/translations/it.json b/homeassistant/components/flick_electric/translations/it.json
index 4adec6d5f6922f..a955849183a12b 100644
--- a/homeassistant/components/flick_electric/translations/it.json
+++ b/homeassistant/components/flick_electric/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo account \u00e8 gi\u00e0 configurato."
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/flick_electric/translations/nl.json b/homeassistant/components/flick_electric/translations/nl.json
index 5f7433d97dbb4d..c4901d328c3c6d 100644
--- a/homeassistant/components/flick_electric/translations/nl.json
+++ b/homeassistant/components/flick_electric/translations/nl.json
@@ -3,7 +3,8 @@
"step": {
"user": {
"data": {
- "password": "Wachtwoord"
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
}
}
}
diff --git a/homeassistant/components/flick_electric/translations/no.json b/homeassistant/components/flick_electric/translations/no.json
index 706ab084901ad8..a0101588e23e20 100644
--- a/homeassistant/components/flick_electric/translations/no.json
+++ b/homeassistant/components/flick_electric/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Denne kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/flick_electric/translations/pl.json b/homeassistant/components/flick_electric/translations/pl.json
index fb6554d00d8d0d..19c319a366bed1 100644
--- a/homeassistant/components/flick_electric/translations/pl.json
+++ b/homeassistant/components/flick_electric/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json
index 1f88596bfc006e..5883c3866968ea 100644
--- a/homeassistant/components/flick_electric/translations/ru.json
+++ b/homeassistant/components/flick_electric/translations/ru.json
@@ -4,7 +4,7 @@
"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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/flick_electric/translations/zh-Hant.json b/homeassistant/components/flick_electric/translations/zh-Hant.json
index 0f88c07cdaa6f7..9e7b29b489315f 100644
--- a/homeassistant/components/flick_electric/translations/zh-Hant.json
+++ b/homeassistant/components/flick_electric/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py
index 4af91a8ef77237..a8bac49867471b 100644
--- a/homeassistant/components/flo/binary_sensor.py
+++ b/homeassistant/components/flo/binary_sensor.py
@@ -36,12 +36,13 @@ def is_on(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
- if self._device.has_alerts:
- attr["info"] = self._device.pending_info_alerts_count
- attr["warning"] = self._device.pending_warning_alerts_count
- attr["critical"] = self._device.pending_critical_alerts_count
- return attr
+ if not self._device.has_alerts:
+ return {}
+ return {
+ "info": self._device.pending_info_alerts_count,
+ "warning": self._device.pending_warning_alerts_count,
+ "critical": self._device.pending_critical_alerts_count,
+ }
@property
def device_class(self):
diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json
new file mode 100644
index 00000000000000..6f39806287630f
--- /dev/null
+++ b/homeassistant/components/flo/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/hu.json b/homeassistant/components/flo/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/flo/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/ko.json b/homeassistant/components/flo/translations/ko.json
new file mode 100644
index 00000000000000..7235d67c278011
--- /dev/null
+++ b/homeassistant/components/flo/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ }
+ }
+ }
+ },
+ "title": "flo"
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/nl.json b/homeassistant/components/flo/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/flo/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/pl.json b/homeassistant/components/flo/translations/pl.json
new file mode 100644
index 00000000000000..561b45d65b1893
--- /dev/null
+++ b/homeassistant/components/flo/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "title": "Flo"
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/strings.json b/homeassistant/components/flume/strings.json
index 039b0d11d724c9..67b4a95d0699fc 100644
--- a/homeassistant/components/flume/strings.json
+++ b/homeassistant/components/flume/strings.json
@@ -1,9 +1,9 @@
{
"config": {
"error": {
- "unknown": "Unexpected error",
- "invalid_auth": "Invalid authentication",
- "cannot_connect": "Failed to connect, please try again"
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
@@ -18,7 +18,7 @@
}
},
"abort": {
- "already_configured": "This account is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/flume/translations/ca.json b/homeassistant/components/flume/translations/ca.json
index 71ee4fd2345e1d..e612b29db674aa 100644
--- a/homeassistant/components/flume/translations/ca.json
+++ b/homeassistant/components/flume/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquest compte ja est\u00e0 configurat"
+ "already_configured": "El compte ja ha estat configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/flume/translations/en.json b/homeassistant/components/flume/translations/en.json
index ed24c552d8ffb6..ac7d4335903f6d 100644
--- a/homeassistant/components/flume/translations/en.json
+++ b/homeassistant/components/flume/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "This account is already configured"
+ "already_configured": "Account is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json
index a746d793bc4fd6..fdb7ab8ed9a0c0 100644
--- a/homeassistant/components/flume/translations/fr.json
+++ b/homeassistant/components/flume/translations/fr.json
@@ -12,6 +12,7 @@
"user": {
"data": {
"client_id": "ID du client",
+ "client_secret": "Secret client",
"password": "Mot de passe",
"username": "Nom d'utilisateur"
},
diff --git a/homeassistant/components/flume/translations/it.json b/homeassistant/components/flume/translations/it.json
index 6d9974f9481f94..43b82331840b2c 100644
--- a/homeassistant/components/flume/translations/it.json
+++ b/homeassistant/components/flume/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo account \u00e8 gi\u00e0 configurato."
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json
index 785f392a255e4d..16f842eb371a4d 100644
--- a/homeassistant/components/flume/translations/no.json
+++ b/homeassistant/components/flume/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Denne kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/flume/translations/pl.json b/homeassistant/components/flume/translations/pl.json
index ff1d73fe0ced77..f899b1446a6a95 100644
--- a/homeassistant/components/flume/translations/pl.json
+++ b/homeassistant/components/flume/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json
index 0ed6e839227231..f35579c2dee5b7 100644
--- a/homeassistant/components/flume/translations/ru.json
+++ b/homeassistant/components/flume/translations/ru.json
@@ -4,7 +4,7 @@
"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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/flume/translations/zh-Hant.json b/homeassistant/components/flume/translations/zh-Hant.json
index cc7dea80e52248..7a585b1b618b2e 100644
--- a/homeassistant/components/flume/translations/zh-Hant.json
+++ b/homeassistant/components/flume/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py
index 0a55a21f6edaca..9918858fd93285 100644
--- a/homeassistant/components/flunearyou/config_flow.py
+++ b/homeassistant/components/flunearyou/config_flow.py
@@ -53,8 +53,6 @@ async def async_step_user(self, user_input=None):
)
except FluNearYouError as err:
LOGGER.error("Error while configuring integration: %s", err)
- return self.async_show_form(
- step_id="user", errors={"base": "general_error"}
- )
+ return self.async_show_form(step_id="user", errors={"base": "unknown"})
return self.async_create_entry(title=unique_id, data=user_input)
diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json
index 2a7e59989b0bfa..780f28fcc6f86f 100644
--- a/homeassistant/components/flunearyou/strings.json
+++ b/homeassistant/components/flunearyou/strings.json
@@ -4,12 +4,17 @@
"user": {
"title": "Configure Flu Near You",
"description": "Monitor user-based and CDC repots for a pair of coordinates.",
- "data": { "latitude": "Latitude", "longitude": "Longitude" }
+ "data": {
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
+ }
}
},
- "error": { "general_error": "There was an unknown error." },
+ "error": {
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
"abort": {
"already_configured": "These coordinates are already registered."
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json
index c26c1f55b2cc18..1cb1408cbfbb05 100644
--- a/homeassistant/components/flunearyou/translations/ca.json
+++ b/homeassistant/components/flunearyou/translations/ca.json
@@ -4,7 +4,8 @@
"already_configured": "Les coordenades ja estan registrades"
},
"error": {
- "general_error": "S'ha produ\u00eft un error desconegut."
+ "general_error": "S'ha produ\u00eft un error desconegut.",
+ "unknown": "Error inesperat"
},
"step": {
"user": {
diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json
index 88997a89c90c7d..289c950ce0c40d 100644
--- a/homeassistant/components/flunearyou/translations/en.json
+++ b/homeassistant/components/flunearyou/translations/en.json
@@ -4,7 +4,8 @@
"already_configured": "These coordinates are already registered."
},
"error": {
- "general_error": "There was an unknown error."
+ "general_error": "There was an unknown error.",
+ "unknown": "Unexpected error"
},
"step": {
"user": {
diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json
index cdaa475037d524..c7306db4ec7d9e 100644
--- a/homeassistant/components/flunearyou/translations/es.json
+++ b/homeassistant/components/flunearyou/translations/es.json
@@ -4,7 +4,8 @@
"already_configured": "Estas coordenadas ya est\u00e1n registradas."
},
"error": {
- "general_error": "Se ha producido un error desconocido."
+ "general_error": "Se ha producido un error desconocido.",
+ "unknown": "Error inesperado"
},
"step": {
"user": {
diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json
new file mode 100644
index 00000000000000..2ed64a235440ae
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/et.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Tundmatu viga"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json
index 27789e1b4cfc7f..50300be55e12e8 100644
--- a/homeassistant/components/flunearyou/translations/fr.json
+++ b/homeassistant/components/flunearyou/translations/fr.json
@@ -12,6 +12,7 @@
"latitude": "Latitude",
"longitude": "Longitude"
},
+ "description": "Surveillez les rapports des utilisateurs et du CDC pour des coordonn\u00e9es.",
"title": "Configurer Flu Near You"
}
}
diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json
index fc90199664eca6..f0dcf878649cbf 100644
--- a/homeassistant/components/flunearyou/translations/it.json
+++ b/homeassistant/components/flunearyou/translations/it.json
@@ -4,7 +4,8 @@
"already_configured": "Queste coordinate sono gi\u00e0 registrate."
},
"error": {
- "general_error": "Si \u00e8 verificato un errore sconosciuto."
+ "general_error": "Si \u00e8 verificato un errore sconosciuto.",
+ "unknown": "Errore imprevisto"
},
"step": {
"user": {
diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json
index de344b82d00046..cde9f39c3f94a7 100644
--- a/homeassistant/components/flunearyou/translations/pl.json
+++ b/homeassistant/components/flunearyou/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane."
},
"error": {
- "general_error": "Nieoczekiwany b\u0142\u0105d."
+ "general_error": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/flunearyou/translations/ru.json b/homeassistant/components/flunearyou/translations/ru.json
index b4ff15d4044854..61d208ce9581ea 100644
--- a/homeassistant/components/flunearyou/translations/ru.json
+++ b/homeassistant/components/flunearyou/translations/ru.json
@@ -4,7 +4,8 @@
"already_configured": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b."
},
"error": {
- "general_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ "general_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py
index 19a5791d7cb5be..9a062133718579 100644
--- a/homeassistant/components/folder/sensor.py
+++ b/homeassistant/components/folder/sensor.py
@@ -94,14 +94,13 @@ def icon(self):
@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
- attr = {
+ return {
"path": self._folder_path,
"filter": self._filter_term,
"number_of_files": self._number_of_files,
"bytes": self._size,
"file_list": self._file_list,
}
- return attr
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/forked_daapd/strings.json b/homeassistant/components/forked_daapd/strings.json
index 33f9c7b91aa3f3..e3be0b6795d383 100644
--- a/homeassistant/components/forked_daapd/strings.json
+++ b/homeassistant/components/forked_daapd/strings.json
@@ -6,7 +6,7 @@
"title": "Set up forked-daapd device",
"data": {
"name": "Friendly name",
- "host": "Host",
+ "host": "[%key:common::config_flow::data::host%]",
"port": "API port",
"password": "API password (leave blank if no password)"
}
@@ -17,10 +17,10 @@
"wrong_host_or_port": "Unable to connect. Please check host and port.",
"wrong_password": "Incorrect password.",
"wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0.",
- "unknown_error": "Unknown error."
+ "unknown_error": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"not_forked_daapd": "Device is not a forked-daapd server."
}
},
diff --git a/homeassistant/components/forked_daapd/translations/ca.json b/homeassistant/components/forked_daapd/translations/ca.json
index cf58af459f68e7..1b3792eeb1bd0a 100644
--- a/homeassistant/components/forked_daapd/translations/ca.json
+++ b/homeassistant/components/forked_daapd/translations/ca.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"not_forked_daapd": "El dispositiu no \u00e9s un servidor de forked-daapd."
},
"error": {
- "unknown_error": "Error desconegut.",
+ "unknown_error": "Error inesperat",
"websocket_not_enabled": "El websocket de forked-daapd no est\u00e0 activat.",
"wrong_host_or_port": "No s'ha pogut connectar, verifica l'amfitri\u00f3 i el port.",
"wrong_password": "Contrasenya incorrecta.",
diff --git a/homeassistant/components/forked_daapd/translations/en.json b/homeassistant/components/forked_daapd/translations/en.json
index 0c87c6624ffc36..397836809da8c2 100644
--- a/homeassistant/components/forked_daapd/translations/en.json
+++ b/homeassistant/components/forked_daapd/translations/en.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured.",
+ "already_configured": "Device is already configured",
"not_forked_daapd": "Device is not a forked-daapd server."
},
"error": {
- "unknown_error": "Unknown error.",
+ "unknown_error": "Unexpected error",
"websocket_not_enabled": "forked-daapd server websocket not enabled.",
"wrong_host_or_port": "Unable to connect. Please check host and port.",
"wrong_password": "Incorrect password.",
diff --git a/homeassistant/components/forked_daapd/translations/it.json b/homeassistant/components/forked_daapd/translations/it.json
index f6d4517c7e7014..3f400e0edc50c5 100644
--- a/homeassistant/components/forked_daapd/translations/it.json
+++ b/homeassistant/components/forked_daapd/translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"not_forked_daapd": "Il dispositivo non \u00e8 un server forked-daapd."
},
"error": {
- "unknown_error": "Errore sconosciuto.",
+ "unknown_error": "Errore imprevisto",
"websocket_not_enabled": "websocket del server forked-daapd non abilitato.",
"wrong_host_or_port": "Impossibile connettersi. Si prega di controllare host e porta.",
"wrong_password": "Password errata",
diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json
index ac58ddf96399a9..32ae6fe73ec373 100644
--- a/homeassistant/components/forked_daapd/translations/no.json
+++ b/homeassistant/components/forked_daapd/translations/no.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert.",
+ "already_configured": "Enheten er allerede konfigurert",
"not_forked_daapd": "Enheten er ikke en forked-daapd-server."
},
"error": {
- "unknown_error": "Ukjent feil.",
+ "unknown_error": "Uventet feil",
"websocket_not_enabled": "websocket for forked-daapd server ikke aktivert.",
"wrong_host_or_port": "Kan ikke koble til. Vennligst sjekk vert og port.",
"wrong_password": "Feil passord.",
diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json
index d40e9b282aad2e..d246307e6d006c 100644
--- a/homeassistant/components/forked_daapd/translations/pl.json
+++ b/homeassistant/components/forked_daapd/translations/pl.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "not_forked_daapd": "Urz\u0105dzenie nie jest serwerem forked-daapd."
},
"error": {
"unknown_error": "Nieznany b\u0142\u0105d.",
- "wrong_password": "Nieprawid\u0142owe has\u0142o"
+ "websocket_not_enabled": "Websocket serwera forked-daapd nie jest w\u0142\u0105czony.",
+ "wrong_host_or_port": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a adres hosta i port.",
+ "wrong_password": "Nieprawid\u0142owe has\u0142o",
+ "wrong_server_type": "Integracja forked-daapd wymaga serwera forked-daapd w wersji >= 27.0."
},
+ "flow_title": "Serwer forked-daapd: {name} ( {host})",
"step": {
"user": {
"data": {
@@ -14,7 +19,8 @@
"name": "Przyjazna nazwa",
"password": "Has\u0142o API (pozostaw puste, je\u015bli nie ma has\u0142a)",
"port": "Port API"
- }
+ },
+ "title": "Skonfiguruj urz\u0105dzenie forked-daapd"
}
}
}
diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json
index 89bd71cb0417a9..d5111b8f286ae9 100644
--- a/homeassistant/components/forked_daapd/translations/ru.json
+++ b/homeassistant/components/forked_daapd/translations/ru.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
"not_forked_daapd": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd."
},
"error": {
- "unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.",
"wrong_host_or_port": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.",
"wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json
index ec51bbacea3cf1..2dcced078ed806 100644
--- a/homeassistant/components/forked_daapd/translations/zh-Hant.json
+++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json
@@ -5,7 +5,7 @@
"not_forked_daapd": "\u8a2d\u5099\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002"
},
"error": {
- "unknown_error": "\u672a\u77e5\u932f\u8aa4\u3002",
+ "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4",
"websocket_not_enabled": "forked-daapd \u4f3a\u670d\u5668 websocket \u672a\u958b\u555f\u3002",
"wrong_host_or_port": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u8207\u901a\u8a0a\u57e0\u3002",
"wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002",
diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py
index bae0336a63e254..6f33c9ff5912fe 100644
--- a/homeassistant/components/foursquare/__init__.py
+++ b/homeassistant/components/foursquare/__init__.py
@@ -5,7 +5,12 @@
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
-from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST, HTTP_OK
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ HTTP_BAD_REQUEST,
+ HTTP_CREATED,
+ HTTP_OK,
+)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -55,7 +60,7 @@ def checkin_user(call):
url = f"https://api.foursquare.com/v2/checkins/add?oauth_token={config[CONF_ACCESS_TOKEN]}&v=20160802&m=swarm"
response = requests.post(url, data=call.data, timeout=10)
- if response.status_code not in (HTTP_OK, 201):
+ if response.status_code not in (HTTP_OK, HTTP_CREATED):
_LOGGER.exception(
"Error checking in user. Response %d: %s:",
response.status_code,
diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py
index 0589dfb2ef10ff..d776c34c4f9bdf 100644
--- a/homeassistant/components/freebox/config_flow.py
+++ b/homeassistant/components/freebox/config_flow.py
@@ -92,7 +92,7 @@ async def async_step_link(self, user_input=None):
except HttpRequestError:
_LOGGER.error("Error connecting to the Freebox router at %s", self._host)
- errors["base"] = "connection_failed"
+ errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py
index dc0d808c438011..aeeaba438ffc14 100644
--- a/homeassistant/components/freebox/sensor.py
+++ b/homeassistant/components/freebox/sensor.py
@@ -146,11 +146,12 @@ def __init__(
def async_update_state(self) -> None:
"""Update the Freebox call sensor."""
self._call_list_for_type = []
- for call in self._router.call_list:
- if not call["new"]:
- continue
- if call["type"] == self._sensor_type:
- self._call_list_for_type.append(call)
+ if self._router.call_list:
+ for call in self._router.call_list:
+ if not call["new"]:
+ continue
+ if call["type"] == self._sensor_type:
+ self._call_list_for_type.append(call)
self._state = len(self._call_list_for_type)
diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json
index 0fdc4571a1d8dc..cb48e5322de944 100644
--- a/homeassistant/components/freebox/strings.json
+++ b/homeassistant/components/freebox/strings.json
@@ -15,11 +15,11 @@
},
"error": {
"register_failed": "Failed to register, please try again",
- "connection_failed": "Failed to connect, please try again",
- "unknown": "Unknown error: please retry later"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Host already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/freebox/translations/ca.json b/homeassistant/components/freebox/translations/ca.json
index 264e0ed3038693..2568c3db0b4644 100644
--- a/homeassistant/components/freebox/translations/ca.json
+++ b/homeassistant/components/freebox/translations/ca.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "connection_failed": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"register_failed": "No s'ha pogut registrar, torna-ho a provar",
- "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard"
+ "unknown": "Error inesperat"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json
index cf18dce0870849..c21e3c6b67fe70 100644
--- a/homeassistant/components/freebox/translations/de.json
+++ b/homeassistant/components/freebox/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Host bereits konfiguriert"
},
"error": {
- "connection_failed": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
"register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut",
"unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut"
},
diff --git a/homeassistant/components/freebox/translations/en.json b/homeassistant/components/freebox/translations/en.json
index 15e18a8982b3ef..539cfbcfe1ee09 100644
--- a/homeassistant/components/freebox/translations/en.json
+++ b/homeassistant/components/freebox/translations/en.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Host already configured"
+ "already_configured": "Device is already configured"
},
"error": {
- "connection_failed": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"register_failed": "Failed to register, please try again",
- "unknown": "Unknown error: please retry later"
+ "unknown": "Unexpected error"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/es-419.json b/homeassistant/components/freebox/translations/es-419.json
index 015835514536d3..1c99bc8b472c71 100644
--- a/homeassistant/components/freebox/translations/es-419.json
+++ b/homeassistant/components/freebox/translations/es-419.json
@@ -4,7 +4,7 @@
"already_configured": "Host ya configurado"
},
"error": {
- "connection_failed": "No se pudo conectar, intente nuevamente",
+ "cannot_connect": "No se pudo conectar, intente nuevamente",
"register_failed": "No se pudo registrar, intente de nuevo",
"unknown": "Error desconocido: vuelva a intentarlo m\u00e1s tarde"
},
diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json
index 3c62f33c3be27b..45926ebc274a99 100644
--- a/homeassistant/components/freebox/translations/es.json
+++ b/homeassistant/components/freebox/translations/es.json
@@ -4,7 +4,7 @@
"already_configured": "El host ya est\u00e1 configurado."
},
"error": {
- "connection_failed": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
+ "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
"register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo",
"unknown": "Error desconocido: por favor, int\u00e9ntalo de nuevo m\u00e1s"
},
diff --git a/homeassistant/components/freebox/translations/et.json b/homeassistant/components/freebox/translations/et.json
new file mode 100644
index 00000000000000..96e7dc6fd75eca
--- /dev/null
+++ b/homeassistant/components/freebox/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "register_failed": "\u00dchenduse loomine nurjus. Proovi uuesti"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json
index 135d8f4d465fd5..f06cfed6cd7695 100644
--- a/homeassistant/components/freebox/translations/fr.json
+++ b/homeassistant/components/freebox/translations/fr.json
@@ -4,7 +4,7 @@
"already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9"
},
"error": {
- "connection_failed": "Impossible de se connecter, veuillez r\u00e9essayer",
+ "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
"register_failed": "\u00c9chec de l'inscription, veuillez r\u00e9essayer",
"unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard"
},
diff --git a/homeassistant/components/freebox/translations/it.json b/homeassistant/components/freebox/translations/it.json
index 11d27eebd693ca..7dd5e279a87277 100644
--- a/homeassistant/components/freebox/translations/it.json
+++ b/homeassistant/components/freebox/translations/it.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Host gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "connection_failed": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"register_failed": "Errore in fase di registrazione, si prega di riprovare",
- "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi"
+ "unknown": "Errore imprevisto"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json
index ce0bc09989e48f..986f345b3ecc29 100644
--- a/homeassistant/components/freebox/translations/ko.json
+++ b/homeassistant/components/freebox/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
- "connection_failed": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
},
diff --git a/homeassistant/components/freebox/translations/lb.json b/homeassistant/components/freebox/translations/lb.json
index eccc419c79b808..e7c1b1148dce35 100644
--- a/homeassistant/components/freebox/translations/lb.json
+++ b/homeassistant/components/freebox/translations/lb.json
@@ -4,7 +4,7 @@
"already_configured": "Apparat ass scho konfigur\u00e9iert"
},
"error": {
- "connection_failed": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.",
+ "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.",
"register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9ier w.e.g. nach emol",
"unknown": "Onbekannte Feeler: prob\u00e9iertsp\u00e9ider nach emol"
},
diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json
index 62c69997e17626..ea41fcfcd6a8e9 100644
--- a/homeassistant/components/freebox/translations/nl.json
+++ b/homeassistant/components/freebox/translations/nl.json
@@ -4,7 +4,7 @@
"already_configured": "Host is al geconfigureerd."
},
"error": {
- "connection_failed": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
"register_failed": "Registratie is mislukt, probeer het opnieuw",
"unknown": "Onbekende fout: probeer het later nog eens"
},
diff --git a/homeassistant/components/freebox/translations/no.json b/homeassistant/components/freebox/translations/no.json
index 0ec9bf70ecdaf6..f66078583c1dfe 100644
--- a/homeassistant/components/freebox/translations/no.json
+++ b/homeassistant/components/freebox/translations/no.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Verten er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "connection_failed": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"register_failed": "Registrering feilet, vennligst pr\u00f8v igjen",
- "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere"
+ "unknown": "Uventet feil"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/pl.json b/homeassistant/components/freebox/translations/pl.json
index c465e16fcb6de9..73c65a7227b105 100644
--- a/homeassistant/components/freebox/translations/pl.json
+++ b/homeassistant/components/freebox/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "Nazwa hosta lub adres IP"
},
"error": {
- "connection_failed": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
"register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie.",
"unknown": "Nieznany b\u0142\u0105d, spr\u00f3buj ponownie p\u00f3\u017aniej."
},
diff --git a/homeassistant/components/freebox/translations/ru.json b/homeassistant/components/freebox/translations/ru.json
index 1bef863e15f612..4ec5b68516c07d 100644
--- a/homeassistant/components/freebox/translations/ru.json
+++ b/homeassistant/components/freebox/translations/ru.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
- "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435."
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/sl.json b/homeassistant/components/freebox/translations/sl.json
index 0a36450501b4cb..a161bfa494e138 100644
--- a/homeassistant/components/freebox/translations/sl.json
+++ b/homeassistant/components/freebox/translations/sl.json
@@ -4,7 +4,7 @@
"already_configured": "Gostitelj je \u017ee konfiguriran"
},
"error": {
- "connection_failed": "Povezava ni uspela, poskusite znova",
+ "cannot_connect": "Povezava ni uspela, poskusite znova",
"register_failed": "Registracija ni uspela, poskusite znova",
"unknown": "Neznana napaka: poskusite pozneje"
},
diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json
index 6c6cc5c64ecee3..aa43ee660322f7 100644
--- a/homeassistant/components/freebox/translations/sv.json
+++ b/homeassistant/components/freebox/translations/sv.json
@@ -4,7 +4,7 @@
"already_configured": "V\u00e4rden \u00e4r redan konfigurerad."
},
"error": {
- "connection_failed": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen",
+ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen",
"register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen",
"unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare"
},
diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json
index be643ab9fd9ae4..608c5bbcba7ed3 100644
--- a/homeassistant/components/freebox/translations/zh-Hant.json
+++ b/homeassistant/components/freebox/translations/zh-Hant.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "connection_failed": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66",
- "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66"
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"link": {
diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py
index 7db216c32e107e..1246eb4afaf2f5 100644
--- a/homeassistant/components/fritzbox/binary_sensor.py
+++ b/homeassistant/components/fritzbox/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for Fritzbox binary sensors."""
import requests
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_WINDOW,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_DEVICES
from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER
@@ -53,7 +56,7 @@ def name(self):
@property
def device_class(self):
"""Return the class of this sensor."""
- return "window"
+ return DEVICE_CLASS_WINDOW
@property
def is_on(self):
diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json
index 3b287fa38a5fa5..5a537b33e7b8d7 100644
--- a/homeassistant/components/fritzbox/strings.json
+++ b/homeassistant/components/fritzbox/strings.json
@@ -19,7 +19,7 @@
}
},
"abort": {
- "already_in_progress": "AVM FRITZ!Box configuration is already in progress.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "This AVM FRITZ!Box is already configured.",
"not_found": "No supported AVM FRITZ!Box found on the network.",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices."
@@ -28,4 +28,4 @@
"auth_failed": "Username and/or password are incorrect."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json
index af82f1fc582ed8..cc8035d4dde871 100644
--- a/homeassistant/components/fritzbox/translations/ca.json
+++ b/homeassistant/components/fritzbox/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Aquest AVM FRITZ!Box ja est\u00e0 configurat.",
- "already_in_progress": "La configuraci\u00f3 de l'AVM FRITZ!Box ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"not_found": "No s'ha trobat cap AVM FRITZ!Box compatible a la xarxa.",
"not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home."
},
diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json
index cc9b13619a7d8f..90f433a9059a91 100644
--- a/homeassistant/components/fritzbox/translations/en.json
+++ b/homeassistant/components/fritzbox/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "This AVM FRITZ!Box is already configured.",
- "already_in_progress": "AVM FRITZ!Box configuration is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"not_found": "No supported AVM FRITZ!Box found on the network.",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices."
},
diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json
index 3b99e98587139c..26c0f2a35a83c7 100644
--- a/homeassistant/components/fritzbox/translations/it.json
+++ b/homeassistant/components/fritzbox/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Questo AVM FRITZ!Box \u00e8 gi\u00e0 configurato.",
- "already_in_progress": "La configurazione di AVM FRITZ!Box \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"not_found": "Nessun AVM FRITZ!Box supportato trovato sulla rete.",
"not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home."
},
diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json
index 44d8c28418bb9a..8b9b5a5c911586 100644
--- a/homeassistant/components/fritzbox/translations/no.json
+++ b/homeassistant/components/fritzbox/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Denne AVM FRITZ!Box er allerede konfigurert.",
- "already_in_progress": "AVM FRITZ!Box-konfigurasjon p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"not_found": "Ingen st\u00f8ttet AVM FRITZ!Box funnet p\u00e5 nettverket.",
"not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter."
},
diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json
index 635e0a2b891fbe..5c6ecc076b9bed 100644
--- a/homeassistant/components/fritzbox/translations/ru.json
+++ b/homeassistant/components/fritzbox/translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.",
"not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e."
},
diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json
index 415ad9c71bd810..882e2d0b6a66df 100644
--- a/homeassistant/components/fritzbox/translations/zh-Hant.json
+++ b/homeassistant/components/fritzbox/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u6b64 AVM FRITZ!Box \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "AVM FRITZ!Box \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684 AVM FRITZ!Box\u3002",
"not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u8a2d\u5099\u3002"
},
diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py
index c0d010cf37e967..5601ad5d74f0ca 100644
--- a/homeassistant/components/fritzbox_netmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_netmonitor/sensor.py
@@ -98,7 +98,7 @@ def state_attributes(self):
# Don't return attributes if FritzBox is unreachable
if self._state == STATE_UNAVAILABLE:
return {}
- attr = {
+ return {
ATTR_IS_LINKED: self._is_linked,
ATTR_IS_CONNECTED: self._is_connected,
ATTR_EXTERNAL_IP: self._external_ip,
@@ -110,7 +110,6 @@ def state_attributes(self):
ATTR_MAX_BYTE_RATE_UP: self._max_byte_rate_up,
ATTR_MAX_BYTE_RATE_DOWN: self._max_byte_rate_down,
}
- return attr
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 73eb24d08cd40b..66c1b6d997fbe7 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -559,7 +559,7 @@ def websocket_get_themes(hass, connection, msg):
"themes": {
"safe_mode": {
"primary-color": "#db4437",
- "accent-color": "#eeee02",
+ "accent-color": "#ffca28",
}
},
"default_theme": "safe_mode",
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 7ccb606894a51e..6732f6e99c37fc 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
- "requirements": ["home-assistant-frontend==20200909.0"],
+ "requirements": ["home-assistant-frontend==20201001.1"],
"dependencies": [
"api",
"auth",
diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml
index 31eb4d5d1ca1d8..cc0d6bde21645b 100644
--- a/homeassistant/components/frontend/services.yaml
+++ b/homeassistant/components/frontend/services.yaml
@@ -5,7 +5,7 @@ set_theme:
fields:
name:
description: Name of a predefined theme, 'default' or 'none'.
- example: "light"
+ example: "default"
mode:
description: The mode the theme is for, either 'dark' or 'light' (default).
example: "dark"
diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py
index 852528fb3a5207..bc9f0f35b5778e 100644
--- a/homeassistant/components/frontier_silicon/media_player.py
+++ b/homeassistant/components/frontier_silicon/media_player.py
@@ -126,11 +126,6 @@ def fs_device(self):
"""
return AFSAPI(self._device_url, self._password)
- @property
- def should_poll(self):
- """Device should be polled."""
- return True
-
@property
def name(self):
"""Return the device name."""
diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py
index 34a9a13b8d9883..9c089e85811768 100644
--- a/homeassistant/components/garadget/cover.py
+++ b/homeassistant/components/garadget/cover.py
@@ -105,14 +105,14 @@ def __init__(self, hass, args):
self._name = doorconfig["nme"]
self.update()
except requests.exceptions.ConnectionError as ex:
- _LOGGER.error("Unable to connect to server: %(reason)s", dict(reason=ex))
+ _LOGGER.error("Unable to connect to server: %(reason)s", {"reason": ex})
self._state = STATE_OFFLINE
self._available = False
self._name = DEFAULT_NAME
except KeyError:
_LOGGER.warning(
"Garadget device %(device)s seems to be offline",
- dict(device=self.device_id),
+ {"device": self.device_id},
)
self._name = DEFAULT_NAME
self._state = STATE_OFFLINE
@@ -129,11 +129,6 @@ def name(self):
"""Return the name of the cover."""
return self._name
- @property
- def should_poll(self):
- """No polling needed for a demo cover."""
- return True
-
@property
def available(self):
"""Return True if entity is available."""
@@ -235,12 +230,12 @@ def update(self):
self.sensor = status["sensor"]
self._available = True
except requests.exceptions.ConnectionError as ex:
- _LOGGER.error("Unable to connect to server: %(reason)s", dict(reason=ex))
+ _LOGGER.error("Unable to connect to server: %(reason)s", {"reason": ex})
self._state = STATE_OFFLINE
except KeyError:
_LOGGER.warning(
"Garadget device %(device)s seems to be offline",
- dict(device=self.device_id),
+ {"device": self.device_id},
)
self._state = STATE_OFFLINE
diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py
index 48713789c6eb21..9b67801105351d 100644
--- a/homeassistant/components/garmin_connect/sensor.py
+++ b/homeassistant/components/garmin_connect/sensor.py
@@ -121,14 +121,13 @@ def unit_of_measurement(self):
@property
def device_state_attributes(self):
"""Return attributes for sensor."""
- attributes = {}
- if self._data.data:
- attributes = {
- "source": self._data.data["source"],
- "last_synced": self._data.data["lastSyncTimestampGMT"],
- ATTR_ATTRIBUTION: ATTRIBUTION,
- }
- return attributes
+ if not self._data.data:
+ return {}
+ return {
+ "source": self._data.data["source"],
+ "last_synced": self._data.data["lastSyncTimestampGMT"],
+ ATTR_ATTRIBUTION: ATTRIBUTION,
+ }
@property
def device_info(self) -> Dict[str, Any]:
diff --git a/homeassistant/components/garmin_connect/strings.json b/homeassistant/components/garmin_connect/strings.json
index 6aac2686a5c86b..0ec7a3ce04c39e 100644
--- a/homeassistant/components/garmin_connect/strings.json
+++ b/homeassistant/components/garmin_connect/strings.json
@@ -4,10 +4,10 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
- "cannot_connect": "Failed to connect, please try again.",
- "invalid_auth": "Invalid authentication.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"too_many_requests": "Too many requests, retry later.",
- "unknown": "Unexpected error."
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
@@ -20,4 +20,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/garmin_connect/translations/ca.json b/homeassistant/components/garmin_connect/translations/ca.json
index 34d7273ef9626d..73b12090fcf0fa 100644
--- a/homeassistant/components/garmin_connect/translations/ca.json
+++ b/homeassistant/components/garmin_connect/translations/ca.json
@@ -4,10 +4,10 @@
"already_configured": "[%key::common::config_flow::abort::already_configured_account%]"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar.",
- "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"too_many_requests": "Massa sol\u00b7licituds, torna-ho a intentar m\u00e9s tard.",
- "unknown": "Error inesperat."
+ "unknown": "Error inesperat"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/en.json b/homeassistant/components/garmin_connect/translations/en.json
index 3b3b2fcb86585e..c1b563d38f3e2f 100644
--- a/homeassistant/components/garmin_connect/translations/en.json
+++ b/homeassistant/components/garmin_connect/translations/en.json
@@ -4,10 +4,10 @@
"already_configured": "Account is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again.",
- "invalid_auth": "Invalid authentication.",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"too_many_requests": "Too many requests, retry later.",
- "unknown": "Unexpected error."
+ "unknown": "Unexpected error"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/it.json b/homeassistant/components/garmin_connect/translations/it.json
index 62937ed2ab1b88..791de295a80a5c 100644
--- a/homeassistant/components/garmin_connect/translations/it.json
+++ b/homeassistant/components/garmin_connect/translations/it.json
@@ -4,10 +4,10 @@
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare.",
- "invalid_auth": "Autenticazione non valida.",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"too_many_requests": "Troppe richieste, riprovare pi\u00f9 tardi.",
- "unknown": "Errore imprevisto."
+ "unknown": "Errore imprevisto"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json
index 9058d46d02a0bc..aa970261f115b0 100644
--- a/homeassistant/components/garmin_connect/translations/no.json
+++ b/homeassistant/components/garmin_connect/translations/no.json
@@ -4,10 +4,10 @@
"already_configured": "Denne kontoen er allerede konfigurert."
},
"error": {
- "cannot_connect": "Kunne ikke koble til, pr\u00f8v igjen.",
- "invalid_auth": "Ugyldig godkjenning.",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
"too_many_requests": "For mange foresp\u00f8rsler, pr\u00f8v p\u00e5 nytt senere.",
- "unknown": "Uventet feil."
+ "unknown": "Uventet feil"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json
index 982c7b2c50bc1f..5aaa67a913ea4e 100644
--- a/homeassistant/components/garmin_connect/translations/pl.json
+++ b/homeassistant/components/garmin_connect/translations/pl.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json
index c9448e1a3fcd1f..69fa96c2a5e9af 100644
--- a/homeassistant/components/garmin_connect/translations/ru.json
+++ b/homeassistant/components/garmin_connect/translations/ru.json
@@ -4,7 +4,7 @@
"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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
diff --git a/homeassistant/components/garmin_connect/translations/zh-Hant.json b/homeassistant/components/garmin_connect/translations/zh-Hant.json
index c7351932734020..cbf928152aa701 100644
--- a/homeassistant/components/garmin_connect/translations/zh-Hant.json
+++ b/homeassistant/components/garmin_connect/translations/zh-Hant.json
@@ -4,10 +4,10 @@
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
- "invalid_auth": "\u9a57\u8b49\u7121\u6548\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002",
- "unknown": "\u672a\u9810\u671f\u932f\u8aa4\u3002"
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/strings.json b/homeassistant/components/gdacs/strings.json
index 496b996823a996..955936cf986b96 100644
--- a/homeassistant/components/gdacs/strings.json
+++ b/homeassistant/components/gdacs/strings.json
@@ -3,9 +3,13 @@
"step": {
"user": {
"title": "Fill in your filter details.",
- "data": { "radius": "Radius" }
+ "data": {
+ "radius": "Radius"
+ }
}
},
- "abort": { "already_configured": "Location is already configured." }
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
+ }
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/translations/ca.json b/homeassistant/components/gdacs/translations/ca.json
index db6359c881b716..b5c13c0c3675f2 100644
--- a/homeassistant/components/gdacs/translations/ca.json
+++ b/homeassistant/components/gdacs/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/en.json b/homeassistant/components/gdacs/translations/en.json
index 8b4d3522ce204a..4e68f486e4d086 100644
--- a/homeassistant/components/gdacs/translations/en.json
+++ b/homeassistant/components/gdacs/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Location is already configured."
+ "already_configured": "Service is already configured"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/it.json b/homeassistant/components/gdacs/translations/it.json
index 3fad0146c138e2..ac5c8ece1297de 100644
--- a/homeassistant/components/gdacs/translations/it.json
+++ b/homeassistant/components/gdacs/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La posizione \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/no.json b/homeassistant/components/gdacs/translations/no.json
index 372a24c0b385be..036c660af90ac1 100644
--- a/homeassistant/components/gdacs/translations/no.json
+++ b/homeassistant/components/gdacs/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Plasseringen er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/ru.json b/homeassistant/components/gdacs/translations/ru.json
index b946694403784d..efad056d304f6e 100644
--- a/homeassistant/components/gdacs/translations/ru.json
+++ b/homeassistant/components/gdacs/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e."
+ "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": {
diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py
index 2fd8466cc198f4..bb2d86539e98a2 100644
--- a/homeassistant/components/geo_json_events/geo_location.py
+++ b/homeassistant/components/geo_json_events/geo_location.py
@@ -203,7 +203,6 @@ def unit_of_measurement(self):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attributes = {}
- if self._external_id:
- attributes[ATTR_EXTERNAL_ID] = self._external_id
- return attributes
+ if not self._external_id:
+ return {}
+ return {ATTR_EXTERNAL_ID: self._external_id}
diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py
index dc8879b51e594e..415f5f48de583b 100644
--- a/homeassistant/components/geo_location/trigger.py
+++ b/homeassistant/components/geo_location/trigger.py
@@ -2,16 +2,11 @@
import voluptuous as vol
from homeassistant.components.geo_location import DOMAIN
-from homeassistant.const import (
- CONF_EVENT,
- CONF_PLATFORM,
- CONF_SOURCE,
- CONF_ZONE,
- EVENT_STATE_CHANGED,
-)
+from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE
from homeassistant.core import callback
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.config_validation import entity_domain
+from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -45,9 +40,6 @@ async def async_attach_trigger(hass, config, action, automation_info):
@callback
def state_change_listener(event):
"""Handle specific state changes."""
- # Skip if the event is not a geo_location entity.
- if not event.data.get("entity_id").startswith(DOMAIN):
- return
# Skip if the event's source does not match the trigger's source.
from_state = event.data.get("old_state")
to_state = event.data.get("new_state")
@@ -83,4 +75,6 @@ def state_change_listener(event):
event.context,
)
- return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
+ return async_track_state_change_filtered(
+ hass, TrackStates(False, set(), {DOMAIN}), state_change_listener
+ ).async_remove
diff --git a/homeassistant/components/geofency/strings.json b/homeassistant/components/geofency/strings.json
index 1c6a72f27c8383..a7b6649fb6e6db 100644
--- a/homeassistant/components/geofency/strings.json
+++ b/homeassistant/components/geofency/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency."
},
"create_entry": {
diff --git a/homeassistant/components/geofency/translations/ca.json b/homeassistant/components/geofency/translations/ca.json
index 315b1d3be8d314..59eab112331ca7 100644
--- a/homeassistant/components/geofency/translations/ca.json
+++ b/homeassistant/components/geofency/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Geofency.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Geofency.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/geofency/translations/el.json b/homeassistant/components/geofency/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/geofency/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geofency/translations/en.json b/homeassistant/components/geofency/translations/en.json
index dad5e9c77d4926..5fc9983e68ce58 100644
--- a/homeassistant/components/geofency/translations/en.json
+++ b/homeassistant/components/geofency/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json
index 9149d6699bebae..0eb67f304bdf15 100644
--- a/homeassistant/components/geofency/translations/es.json
+++ b/homeassistant/components/geofency/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nRellene la siguiente informaci\u00f3n:\n\n- URL: ``{webhook_url}``\n- M\u00e9todo: POST\n\nVer[la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/geofency/translations/et.json b/homeassistant/components/geofency/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/geofency/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geofency/translations/fr.json b/homeassistant/components/geofency/translations/fr.json
index 142f40754b9ddd..b7940cbe58cab9 100644
--- a/homeassistant/components/geofency/translations/fr.json
+++ b/homeassistant/components/geofency/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Geofency.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonctionnalit\u00e9 Webhook dans Geofency. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
diff --git a/homeassistant/components/geofency/translations/it.json b/homeassistant/components/geofency/translations/it.json
index 4a74e58e5ca998..e061e803e287be 100644
--- a/homeassistant/components/geofency/translations/it.json
+++ b/homeassistant/components/geofency/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli."
diff --git a/homeassistant/components/geofency/translations/lb.json b/homeassistant/components/geofency/translations/lb.json
index 16f973e52603b7..fcf26deb3ada68 100644
--- a/homeassistant/components/geofency/translations/lb.json
+++ b/homeassistant/components/geofency/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Geofency Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Geofency ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/geofency/translations/no.json b/homeassistant/components/geofency/translations/no.json
index 8e66cab4c9c8b5..8bfedb5c0ec27b 100644
--- a/homeassistant/components/geofency/translations/no.json
+++ b/homeassistant/components/geofency/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 kunne sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Geofency. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer."
diff --git a/homeassistant/components/geofency/translations/ru.json b/homeassistant/components/geofency/translations/ru.json
index dd2a20ad61c1ec..80304f3eb26f73 100644
--- a/homeassistant/components/geofency/translations/ru.json
+++ b/homeassistant/components/geofency/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Geofency.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\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]({docs_url}) \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."
diff --git a/homeassistant/components/geofency/translations/zh-Hant.json b/homeassistant/components/geofency/translations/zh-Hant.json
index 95fec4ae15ef5a..494dc8ddf23240 100644
--- a/homeassistant/components/geofency/translations/zh-Hant.json
+++ b/homeassistant/components/geofency/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Geofency \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/geonetnz_quakes/strings.json b/homeassistant/components/geonetnz_quakes/strings.json
index fe328c05603be1..3f2d702492f981 100644
--- a/homeassistant/components/geonetnz_quakes/strings.json
+++ b/homeassistant/components/geonetnz_quakes/strings.json
@@ -6,6 +6,6 @@
"data": { "radius": "Radius", "mmi": "MMI" }
}
},
- "abort": { "already_configured": "Location is already configured." }
+ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
}
}
diff --git a/homeassistant/components/geonetnz_quakes/translations/ca.json b/homeassistant/components/geonetnz_quakes/translations/ca.json
index e97142a6e3fe9d..2a0aff80dfd737 100644
--- a/homeassistant/components/geonetnz_quakes/translations/ca.json
+++ b/homeassistant/components/geonetnz_quakes/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/en.json b/homeassistant/components/geonetnz_quakes/translations/en.json
index 68f73bcf0898c2..18ba6c06f6d145 100644
--- a/homeassistant/components/geonetnz_quakes/translations/en.json
+++ b/homeassistant/components/geonetnz_quakes/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Location is already configured."
+ "already_configured": "Service is already configured"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/it.json b/homeassistant/components/geonetnz_quakes/translations/it.json
index c07f04cdb644e3..a78f8f9fa2ddd9 100644
--- a/homeassistant/components/geonetnz_quakes/translations/it.json
+++ b/homeassistant/components/geonetnz_quakes/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La posizione \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/no.json b/homeassistant/components/geonetnz_quakes/translations/no.json
index fc3b339d807e07..24b3fb5d70c4b6 100644
--- a/homeassistant/components/geonetnz_quakes/translations/no.json
+++ b/homeassistant/components/geonetnz_quakes/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Plasseringen er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/ru.json b/homeassistant/components/geonetnz_quakes/translations/ru.json
index 7ee4f64431e331..6e6fb5fe8ef142 100644
--- a/homeassistant/components/geonetnz_quakes/translations/ru.json
+++ b/homeassistant/components/geonetnz_quakes/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json
index 2187bcbc9980ca..34e99570dd041e 100644
--- a/homeassistant/components/gios/strings.json
+++ b/homeassistant/components/gios/strings.json
@@ -5,7 +5,7 @@
"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": "Name of the integration",
+ "name": "[%key:common::config_flow::data::name%]",
"station_id": "ID of the measuring station"
}
}
@@ -13,10 +13,10 @@
"error": {
"wrong_station_id": "ID of the measuring station is not correct.",
"invalid_sensors_data": "Invalid sensors' data for this measuring station.",
- "cannot_connect": "Cannot connect to the GIO\u015a server."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "GIO\u015a integration for this measuring station is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%] for this measuring station"
}
}
}
diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json
index 29703281b087ce..06e405d78c6003 100644
--- a/homeassistant/components/gios/translations/ca.json
+++ b/homeassistant/components/gios/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "La integraci\u00f3 GIO\u015a per a aquesta estaci\u00f3 ja est\u00e0 configurada."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar al servidor de GIO\u015a.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_sensors_data": "Les dades dels sensors d'aquesta estaci\u00f3 de mesura s\u00f3n inv\u00e0lides.",
"wrong_station_id": "L'ID de l'estaci\u00f3 de mesura \u00e9s incorrecte."
},
diff --git a/homeassistant/components/gios/translations/en.json b/homeassistant/components/gios/translations/en.json
index 3d07ad843bd1aa..b3a3842db83c5a 100644
--- a/homeassistant/components/gios/translations/en.json
+++ b/homeassistant/components/gios/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "GIO\u015a integration for this measuring station is already configured."
+ "already_configured": "Service is already configured"
},
"error": {
- "cannot_connect": "Cannot connect to the GIO\u015a server.",
+ "cannot_connect": "Failed to connect",
"invalid_sensors_data": "Invalid sensors' data for this measuring station.",
"wrong_station_id": "ID of the measuring station is not correct."
},
diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json
index e49fe2ebfe8d46..72df1b3d01d1fb 100644
--- a/homeassistant/components/gios/translations/it.json
+++ b/homeassistant/components/gios/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "L'integrazione GIO\u015a per questa stazione di misurazione \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi al server GIO\u015a.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_sensors_data": "Dati dei sensori non validi per questa stazione di misura.",
"wrong_station_id": "L'ID della stazione di misura non \u00e8 corretto."
},
diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json
index 784b75c9ee5478..de3e038eaaf73c 100644
--- a/homeassistant/components/gios/translations/no.json
+++ b/homeassistant/components/gios/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "GIO\u015a-integrasjon for denne m\u00e5lestasjonen er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Kan ikke koble til GIO\u015a-tjeneren",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_sensors_data": "Ugyldig sensordata for denne m\u00e5lestasjonen",
"wrong_station_id": "ID for m\u00e5lestasjon er ikke korrekt"
},
diff --git a/homeassistant/components/gios/translations/ru.json b/homeassistant/components/gios/translations/ru.json
index ca94b617c9374e..50a8560b00cc1c 100644
--- a/homeassistant/components/gios/translations/ru.json
+++ b/homeassistant/components/gios/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.",
"wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438."
},
diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json
index 0d75f83f9e529c..5642c1250ba97f 100644
--- a/homeassistant/components/gios/translations/zh-Hant.json
+++ b/homeassistant/components/gios/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64 GIO\u015a \u76e3\u6e2c\u7ad9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 GIO\u015a \u4f3a\u670d\u5668\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002",
"wrong_station_id": "\u76e3\u6e2c\u7ad9 ID \u4e0d\u6b63\u78ba\u3002"
},
diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json
index 68f59c50f985ef..5d96b1ae57e172 100644
--- a/homeassistant/components/glances/strings.json
+++ b/homeassistant/components/glances/strings.json
@@ -4,23 +4,23 @@
"user": {
"title": "Setup Glances",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"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%]",
"version": "Glances API Version (2 or 3)",
- "ssl": "Use SSL/TLS to connect to the Glances system",
- "verify_ssl": "Verify the certification of the system"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
"error": {
- "cannot_connect": "Unable to connect to host",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"wrong_version": "Version not supported (2 or 3 only)"
},
"abort": {
- "already_configured": "Host is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/glances/translations/ca.json b/homeassistant/components/glances/translations/ca.json
index 7da63024f8b39c..1ef17e201a4abf 100644
--- a/homeassistant/components/glances/translations/ca.json
+++ b/homeassistant/components/glances/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat."
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "Nom",
"password": "Contrasenya",
"port": "Port",
- "ssl": "Utilitza SSL/TLS per connectar-te al sistema Glances",
+ "ssl": "Utilitza un certificat SSL",
"username": "Nom d'usuari",
- "verify_ssl": "Verifica la certificaci\u00f3 del sistema",
+ "verify_ssl": "Verifica el certificat SSL",
"version": "Versi\u00f3 de l'API de Glances (2 o 3)"
},
"title": "Configuraci\u00f3 de Glances"
diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json
index 0330e8cef653b2..87c53c3cf48a2f 100644
--- a/homeassistant/components/glances/translations/en.json
+++ b/homeassistant/components/glances/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Host is already configured."
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Unable to connect to host",
+ "cannot_connect": "Failed to connect",
"wrong_version": "Version not supported (2 or 3 only)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "Name",
"password": "Password",
"port": "Port",
- "ssl": "Use SSL/TLS to connect to the Glances system",
+ "ssl": "Uses an SSL certificate",
"username": "Username",
- "verify_ssl": "Verify the certification of the system",
+ "verify_ssl": "Verify SSL certificate",
"version": "Glances API Version (2 or 3)"
},
"title": "Setup Glances"
diff --git a/homeassistant/components/glances/translations/it.json b/homeassistant/components/glances/translations/it.json
index 7e8d5af6d8fd71..f4806c5d95266c 100644
--- a/homeassistant/components/glances/translations/it.json
+++ b/homeassistant/components/glances/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "L'host \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi all'host",
+ "cannot_connect": "Impossibile connettersi",
"wrong_version": "Versione non supportata (solo 2 o 3)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "Nome",
"password": "Password",
"port": "Porta",
- "ssl": "Utilizzare SSL/TLS per connettersi al sistema Glances",
+ "ssl": "Utilizza un certificato SSL",
"username": "Nome utente",
- "verify_ssl": "Verificare la certificazione del sistema",
+ "verify_ssl": "Verificare il certificato SSL",
"version": "Glances API Version (2 o 3)"
},
"title": "Impostare Glances"
diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json
index dd593c4add6f03..bac0387549a43d 100644
--- a/homeassistant/components/glances/translations/no.json
+++ b/homeassistant/components/glances/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Verten er allerede konfigurert."
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Kan ikke koble til vert",
+ "cannot_connect": "Tilkobling mislyktes.",
"wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "Navn",
"password": "Passord",
"port": "",
- "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet",
+ "ssl": "Bruker et SSL-sertifikat",
"username": "Brukernavn",
- "verify_ssl": "Bekreft sertifiseringen av systemet",
+ "verify_ssl": "Verifisere SSL-sertifikat",
"version": "Glances API-versjon (2 eller 3)"
},
"title": "Oppsett av Glances"
diff --git a/homeassistant/components/glances/translations/ru.json b/homeassistant/components/glances/translations/ru.json
index d87bcb536cf7cd..0dc8c72dc9f55e 100644
--- a/homeassistant/components/glances/translations/ru.json
+++ b/homeassistant/components/glances/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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 \u043a \u0445\u043e\u0441\u0442\u0443.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3."
},
"step": {
@@ -14,9 +14,9 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"username": "\u041b\u043e\u0433\u0438\u043d",
- "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u044b",
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)"
},
"title": "Glances"
diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json
index dd7c3711e3733b..0054edbdb0d9e6 100644
--- a/homeassistant/components/glances/translations/zh-Hant.json
+++ b/homeassistant/components/glances/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09"
},
"step": {
@@ -14,9 +14,9 @@
"name": "\u540d\u7a31",
"password": "\u5bc6\u78bc",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 Glances \u7cfb\u7d71",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"username": "\u4f7f\u7528\u8005\u540d\u7a31",
- "verify_ssl": "\u9a57\u8b49\u7cfb\u7d71\u8a8d\u8b49",
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49",
"version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09"
},
"title": "\u8a2d\u5b9a Glances"
diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py
new file mode 100644
index 00000000000000..ff60a9ac0435bf
--- /dev/null
+++ b/homeassistant/components/goalzero/__init__.py
@@ -0,0 +1,112 @@
+"""The Goal Zero Yeti integration."""
+import asyncio
+import logging
+
+from goalzero import Yeti, exceptions
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, MIN_TIME_BETWEEN_UPDATES
+
+_LOGGER = logging.getLogger(__name__)
+
+
+PLATFORMS = ["binary_sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config):
+ """Set up the Goal Zero Yeti component."""
+
+ hass.data[DOMAIN] = {}
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Goal Zero Yeti from a config entry."""
+ name = entry.data[CONF_NAME]
+ host = entry.data[CONF_HOST]
+
+ session = async_get_clientsession(hass)
+ api = Yeti(host, hass.loop, session)
+ try:
+ await api.get_state()
+ except exceptions.ConnectError as ex:
+ _LOGGER.warning("Failed to connect: %s", ex)
+ raise ConfigEntryNotReady from ex
+
+ async def async_update_data():
+ """Fetch data from API endpoint."""
+ try:
+ await api.get_state()
+ except exceptions.ConnectError as err:
+ raise UpdateFailed(f"Failed to communicating with API: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=name,
+ update_method=async_update_data,
+ update_interval=MIN_TIME_BETWEEN_UPDATES,
+ )
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_KEY_API: api,
+ DATA_KEY_COORDINATOR: coordinator,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unload_ok
+
+
+class YetiEntity(CoordinatorEntity):
+ """Representation of a Goal Zero Yeti entity."""
+
+ def __init__(self, api, coordinator, name, server_unique_id):
+ """Initialize a Goal Zero Yeti entity."""
+ super().__init__(coordinator)
+ self.api = api
+ self._name = name
+ self._server_unique_id = server_unique_id
+ self._device_class = None
+
+ @property
+ def device_info(self):
+ """Return the device information of the entity."""
+ return {
+ "identifiers": {(DOMAIN, self._server_unique_id)},
+ "name": self._name,
+ "manufacturer": "Goal Zero",
+ }
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return self._device_class
diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py
new file mode 100644
index 00000000000000..a2af8a18546bba
--- /dev/null
+++ b/homeassistant/components/goalzero/binary_sensor.py
@@ -0,0 +1,62 @@
+"""Support for Goal Zero Yeti Sensors."""
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.const import CONF_NAME
+
+from . import YetiEntity
+from .const import BINARY_SENSOR_DICT, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the Goal Zero Yeti sensor."""
+ name = entry.data[CONF_NAME]
+ goalzero_data = hass.data[DOMAIN][entry.entry_id]
+ sensors = [
+ YetiBinarySensor(
+ goalzero_data[DATA_KEY_API],
+ goalzero_data[DATA_KEY_COORDINATOR],
+ name,
+ sensor_name,
+ entry.entry_id,
+ )
+ for sensor_name in BINARY_SENSOR_DICT
+ ]
+ async_add_entities(sensors, True)
+
+
+class YetiBinarySensor(YetiEntity, BinarySensorEntity):
+ """Representation of a Goal Zero Yeti sensor."""
+
+ def __init__(self, api, coordinator, name, sensor_name, server_unique_id):
+ """Initialize a Goal Zero Yeti sensor."""
+ super().__init__(api, coordinator, name, server_unique_id)
+
+ self._condition = sensor_name
+
+ variable_info = BINARY_SENSOR_DICT[sensor_name]
+ self._condition_name = variable_info[0]
+ self._icon = variable_info[2]
+ self._device_class = variable_info[1]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._name} {self._condition_name}"
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return f"{self._server_unique_id}/{self._condition_name}"
+
+ @property
+ def is_on(self):
+ """Return if the service is on."""
+ if self.api.data:
+ return self.api.data[self._condition] == 1
+ return False
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return self._icon
diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py
new file mode 100644
index 00000000000000..35b6953865c35b
--- /dev/null
+++ b/homeassistant/components/goalzero/config_flow.py
@@ -0,0 +1,77 @@
+"""Config flow for Goal Zero Yeti integration."""
+import logging
+
+from goalzero import Yeti, exceptions
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DEFAULT_NAME, DOMAIN # pylint:disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema({"host": str, "name": str})
+
+
+class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Goal Zero Yeti."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ errors = {}
+
+ if user_input is not None:
+ host = user_input[CONF_HOST]
+ name = user_input[CONF_NAME]
+
+ if await self._async_endpoint_existed(host):
+ return self.async_abort(reason="already_configured")
+
+ try:
+ await self._async_try_connect(host)
+ except exceptions.ConnectError:
+ errors["base"] = "cannot_connect"
+ _LOGGER.error("Error connecting to device at %s", host)
+ except exceptions.InvalidHost:
+ errors["base"] = "invalid_host"
+ _LOGGER.error("Invalid host at %s", host)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ return self.async_create_entry(
+ title=name,
+ data={CONF_HOST: host, CONF_NAME: name},
+ )
+
+ user_input = user_input or {}
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_HOST, default=user_input.get(CONF_HOST) or ""
+ ): str,
+ vol.Optional(
+ CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME
+ ): str,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def _async_endpoint_existed(self, endpoint):
+ for entry in self._async_current_entries():
+ if endpoint == entry.data.get(CONF_HOST):
+ return True
+ return False
+
+ async def _async_try_connect(self, host):
+ session = async_get_clientsession(self.hass)
+ api = Yeti(host, self.hass.loop, session)
+ await api.get_state()
diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py
new file mode 100644
index 00000000000000..3afa1e537c173d
--- /dev/null
+++ b/homeassistant/components/goalzero/const.py
@@ -0,0 +1,28 @@
+"""Constants for the Goal Zero Yeti integration."""
+from datetime import timedelta
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_BATTERY_CHARGING,
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_POWER,
+)
+
+DATA_KEY_COORDINATOR = "coordinator"
+DOMAIN = "goalzero"
+DEFAULT_NAME = "Yeti"
+DATA_KEY_API = "api"
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
+
+BINARY_SENSOR_DICT = {
+ "v12PortStatus": ["12V Port Status", DEVICE_CLASS_POWER, None],
+ "usbPortStatus": ["USB Port Status", DEVICE_CLASS_POWER, None],
+ "acPortStatus": ["AC Port Status", DEVICE_CLASS_POWER, None],
+ "backlight": ["Backlight", None, "mdi:clock-digital"],
+ "app_online": [
+ "App Online",
+ DEVICE_CLASS_CONNECTIVITY,
+ None,
+ ],
+ "isCharging": ["Charging", DEVICE_CLASS_BATTERY_CHARGING, None],
+}
diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json
new file mode 100644
index 00000000000000..803b8f7eaae01d
--- /dev/null
+++ b/homeassistant/components/goalzero/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "goalzero",
+ "name": "Goal Zero Yeti",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/goalzero",
+ "requirements": ["goalzero==0.1.4"],
+ "codeowners": ["@tkdrob"]
+}
diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json
new file mode 100644
index 00000000000000..c16b3b283b5ab3
--- /dev/null
+++ b/homeassistant/components/goalzero/strings.json
@@ -0,0 +1,22 @@
+{
+ "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 Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "name": "[%key:common::config_flow::data::name%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_host": "Invalid host provided",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
+ }
+ }
+}
diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json
new file mode 100644
index 00000000000000..00999a0b54eacd
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_host": "L'amfitri\u00f3 proporcionat no \u00e9s v\u00e0lid",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "name": "Nom"
+ },
+ "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el teu Yeti a la teva xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu encaminador (router). Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del teu encaminador.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json
new file mode 100644
index 00000000000000..61936c6ff56080
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/el.json
@@ -0,0 +1,22 @@
+{
+ "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"
+ },
+ "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"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "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.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json
new file mode 100644
index 00000000000000..0bba36a58c6ffc
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_host": "Invalid host provided",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name"
+ },
+ "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 Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json
new file mode 100644
index 00000000000000..4897899d8c36ba
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/es.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La cuenta ya ha sido configurada"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_host": "Este no es el Yeti que est\u00e1s buscando",
+ "unknown": "Error desconocido"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nombre"
+ },
+ "description": "Primero, tienes que descargar la aplicaci\u00f3n Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSigue las instrucciones para conectar tu Yeti a tu red Wifi. Luego obt\u00e9n la IP de tu router. El DHCP debe estar configurado en los ajustes de tu router para asegurar que la IP de host del dispositivo no cambie. Consulta el manual de usuario de tu router.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json
new file mode 100644
index 00000000000000..7c995db2529d25
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/et.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus",
+ "unknown": "Tundmatu viga"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json
new file mode 100644
index 00000000000000..a155e8370d1037
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/fr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "unknown": "Erreur inconnue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "name": "Nom"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json
new file mode 100644
index 00000000000000..22d081e9c4f77f
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_host": "Host fornito non valido",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nome"
+ },
+ "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 il dispositivo assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/lb.json b/homeassistant/components/goalzero/translations/lb.json
new file mode 100644
index 00000000000000..5972c69873b72f
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/lb.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kont ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_host": "D\u00ebst ass net de gesichte Yeti",
+ "unknown": "Onbekannte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Numm"
+ },
+ "description": "Fir d'\u00e9ischt muss Goal Zero App erofgeluede ginn:\nhttps://www.goalzero.com/product-features/yeti-app/\n\nFolleg d'Instruktioune fir d\u00e4in Yeti mat dengem Wifi ze verbannen. Dann erm\u00ebttel d'IP vum Yeti an dengem Router. DHCP muss aktiv sinn an de Yeti Apparat sollt \u00ebmmer d\u00e9iselwecht IP zougewise kr\u00e9ien. Kuck dat am Guide vun dengen Router Astellungen no.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json
new file mode 100644
index 00000000000000..86d873ef5f7b52
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_host": "Ugyldig vert gitt",
+ "unknown": "Ukjent feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "name": "Navn"
+ },
+ "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-ip fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se brukerh\u00e5ndboken til ruteren.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json
new file mode 100644
index 00000000000000..a5f2edec12e434
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_host": "Podano nieprawid\u0142owy host",
+ "unknown": "Nieznany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Nast\u0119pnie uzyskaj adres IP hosta z routera. W ustawieniach routera nale\u017cy skonfigurowa\u0107 DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json
new file mode 100644
index 00000000000000..80c349f4c0f1d5
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/ru.json
@@ -0,0 +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."
+ },
+ "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_host": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0445\u043e\u0441\u0442.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
+ },
+ "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u0417\u0430\u0442\u0435\u043c \u0443\u0437\u043d\u0430\u0439\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json
new file mode 100644
index 00000000000000..8eb51501659690
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_host": "\u6240\u63d0\u4f9b\u7684\u4e3b\u6a5f\u7aef\u7121\u6548",
+ "unknown": "\u672a\u77e5\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "name": "\u540d\u7a31"
+ },
+ "description": "\u60a8\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u63a5\u8005\u7531\u8def\u7531\u5668\u53d6\u5f97\u4e3b\u6a5f\u7aef IP\uff0c \u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u8a2d\u5099\u7684 DHCP \u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/const.py b/homeassistant/components/gogogate2/const.py
index 5c0ef55ff3fc0b..2f6ac76122fcd6 100644
--- a/homeassistant/components/gogogate2/const.py
+++ b/homeassistant/components/gogogate2/const.py
@@ -4,3 +4,4 @@
DATA_UPDATE_COORDINATOR = "data_update_coordinator"
DEVICE_TYPE_GOGOGATE2 = "gogogate2"
DEVICE_TYPE_ISMARTGATE = "ismartgate"
+MANUFACTURER = "Remsol"
diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py
index 8e753eb6ae5eb0..e748b420edbe8a 100644
--- a/homeassistant/components/gogogate2/cover.py
+++ b/homeassistant/components/gogogate2/cover.py
@@ -34,7 +34,7 @@
cover_unique_id,
get_data_update_coordinator,
)
-from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN
+from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@@ -154,3 +154,15 @@ def _get_door(self) -> AbstractDoor:
door = get_door_by_id(self._door.door_id, self.coordinator.data)
self._door = door or self._door
return self._door
+
+ @property
+ def device_info(self):
+ """Device info for the controller."""
+ data = self.coordinator.data
+ return {
+ "identifiers": {(DOMAIN, self._config_entry.unique_id)},
+ "name": self._config_entry.title,
+ "manufacturer": MANUFACTURER,
+ "model": data.model,
+ "sw_version": data.firmwareversion,
+ }
diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json
index edf69144f624c8..893294da25e699 100644
--- a/homeassistant/components/gogogate2/manifest.json
+++ b/homeassistant/components/gogogate2/manifest.json
@@ -3,7 +3,7 @@
"name": "Gogogate2 and iSmartGate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gogogate2",
- "requirements": ["gogogate2-api==2.0.1"],
+ "requirements": ["gogogate2-api==2.0.3"],
"codeowners": ["@vangorra"],
"homekit": {
"models": [
diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json
index 47a53d0d320988..f5385ff5d54249 100644
--- a/homeassistant/components/gogogate2/strings.json
+++ b/homeassistant/components/gogogate2/strings.json
@@ -12,7 +12,7 @@
"title": "Setup GogoGate2 or iSmartGate",
"description": "Provide requisite information below.",
"data": {
- "ip_address": "IP Address",
+ "ip_address": "[%key:common::config_flow::data::ip%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json
index 478e7e8ccf8004..79f216738c4cc9 100644
--- a/homeassistant/components/gogogate2/translations/fr.json
+++ b/homeassistant/components/gogogate2/translations/fr.json
@@ -15,7 +15,7 @@
"username": "Nom d'utilisateur"
},
"description": "Fournissez les informations requises ci-dessous.",
- "title": "Configurer GogoGate2"
+ "title": "Configurer GogoGate2 ou iSmartGate"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json
index 7a6c33be781ae4..2ea10491255858 100644
--- a/homeassistant/components/gogogate2/translations/pl.json
+++ b/homeassistant/components/gogogate2/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index 7b75a36f8bb928..4bf0df8b933022 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -10,7 +10,11 @@
# Typing imports
from homeassistant.components.http import HomeAssistantView
-from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_INTERNAL_SERVER_ERROR
+from homeassistant.const import (
+ CLOUD_NEVER_EXPOSED_ENTITIES,
+ HTTP_INTERNAL_SERVER_ERROR,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
@@ -200,7 +204,7 @@ async def _call():
try:
return await _call()
except ClientResponseError as error:
- if error.status == 401:
+ if error.status == HTTP_UNAUTHORIZED:
_LOGGER.warning(
"Request for %s unauthorized, renewing token and retrying", url
)
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index 5d2d59f21449de..653324758e0fc8 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -29,6 +29,7 @@
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
+ CAST_APP_ID_HOMEASSISTANT,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
SERVICE_ALARM_ARM_HOME,
@@ -287,7 +288,10 @@ async def execute(self, command, data, params, challenge):
url = await self.hass.components.camera.async_request_stream(
self.state.entity_id, "hls"
)
- self.stream_info = {"cameraStreamAccessUrl": f"{get_url(self.hass)}{url}"}
+ self.stream_info = {
+ "cameraStreamAccessUrl": f"{get_url(self.hass)}{url}",
+ "cameraStreamReceiverAppId": CAST_APP_ID_HOMEASSISTANT,
+ }
@register_trait
diff --git a/homeassistant/components/gpslogger/strings.json b/homeassistant/components/gpslogger/strings.json
index f3d4344cd49c85..b757e5c46cb735 100644
--- a/homeassistant/components/gpslogger/strings.json
+++ b/homeassistant/components/gpslogger/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger."
},
"create_entry": {
diff --git a/homeassistant/components/gpslogger/translations/ca.json b/homeassistant/components/gpslogger/translations/ca.json
index aebd839a83710c..96edeea6ccb0fd 100644
--- a/homeassistant/components/gpslogger/translations/ca.json
+++ b/homeassistant/components/gpslogger/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de GPSLogger.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de GPSLogger.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/gpslogger/translations/el.json b/homeassistant/components/gpslogger/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpslogger/translations/en.json b/homeassistant/components/gpslogger/translations/en.json
index 46bbb23148324c..197c012ed4099a 100644
--- a/homeassistant/components/gpslogger/translations/en.json
+++ b/homeassistant/components/gpslogger/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/gpslogger/translations/es.json b/homeassistant/components/gpslogger/translations/es.json
index 8ac817cacbfe95..41199925e1c455 100644
--- a/homeassistant/components/gpslogger/translations/es.json
+++ b/homeassistant/components/gpslogger/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en GPSLogger.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/gpslogger/translations/et.json b/homeassistant/components/gpslogger/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpslogger/translations/fr.json b/homeassistant/components/gpslogger/translations/fr.json
index 65db2a5a300089..bb9321c5fcca07 100644
--- a/homeassistant/components/gpslogger/translations/fr.json
+++ b/homeassistant/components/gpslogger/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages GPSLogger.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans GPSLogger. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
diff --git a/homeassistant/components/gpslogger/translations/it.json b/homeassistant/components/gpslogger/translations/it.json
index 32e6b574096aaa..dac92f90262a21 100644
--- a/homeassistant/components/gpslogger/translations/it.json
+++ b/homeassistant/components/gpslogger/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da GPSLogger.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli."
diff --git a/homeassistant/components/gpslogger/translations/lb.json b/homeassistant/components/gpslogger/translations/lb.json
index a69ac61bc988eb..2e4362b18e2ebe 100644
--- a/homeassistant/components/gpslogger/translations/lb.json
+++ b/homeassistant/components/gpslogger/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir GPSLogger Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am GPSLogger ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/gpslogger/translations/no.json b/homeassistant/components/gpslogger/translations/no.json
index 2b3dc0f67fc2f5..bfd40a5d0f884c 100644
--- a/homeassistant/components/gpslogger/translations/no.json
+++ b/homeassistant/components/gpslogger/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra GPSLogger.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i GPSLogger. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer."
diff --git a/homeassistant/components/gpslogger/translations/ru.json b/homeassistant/components/gpslogger/translations/ru.json
index fabf2477590137..bfa6a4850b39ad 100644
--- a/homeassistant/components/gpslogger/translations/ru.json
+++ b/homeassistant/components/gpslogger/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 GPSLogger.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\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]({docs_url}) \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."
diff --git a/homeassistant/components/gpslogger/translations/zh-Hant.json b/homeassistant/components/gpslogger/translations/zh-Hant.json
index f648d20df9fb82..59aa99d1689ac4 100644
--- a/homeassistant/components/gpslogger/translations/zh-Hant.json
+++ b/homeassistant/components/gpslogger/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc GPSLogger \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/griddy/sensor.py b/homeassistant/components/griddy/sensor.py
index acdcefee52784b..7a155586fac6c1 100644
--- a/homeassistant/components/griddy/sensor.py
+++ b/homeassistant/components/griddy/sensor.py
@@ -1,7 +1,7 @@
"""Support for August sensors."""
import logging
-from homeassistant.const import ENERGY_KILO_WATT_HOUR
+from homeassistant.const import CURRENCY_CENT, ENERGY_KILO_WATT_HOUR
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_LOADZONE, DOMAIN
@@ -29,7 +29,7 @@ def __init__(self, settlement_point, coordinator):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- return f"¢/{ENERGY_KILO_WATT_HOUR}"
+ return f"{CURRENCY_CENT}/{ENERGY_KILO_WATT_HOUR}"
@property
def name(self):
diff --git a/homeassistant/components/griddy/strings.json b/homeassistant/components/griddy/strings.json
index d8ccb94fae7294..4bc06aade1d418 100644
--- a/homeassistant/components/griddy/strings.json
+++ b/homeassistant/components/griddy/strings.json
@@ -1,8 +1,8 @@
{
"config": {
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
diff --git a/homeassistant/components/griddy/translations/ca.json b/homeassistant/components/griddy/translations/ca.json
index cb363a7dfabe58..8ec47b83bb2158 100644
--- a/homeassistant/components/griddy/translations/ca.json
+++ b/homeassistant/components/griddy/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "Aquesta zona de c\u00e0rrega ja est\u00e0 configurada"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
"step": {
diff --git a/homeassistant/components/griddy/translations/en.json b/homeassistant/components/griddy/translations/en.json
index bb1e217133a69f..e9cc3e7bb6ba28 100644
--- a/homeassistant/components/griddy/translations/en.json
+++ b/homeassistant/components/griddy/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "This Load Zone is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
diff --git a/homeassistant/components/griddy/translations/it.json b/homeassistant/components/griddy/translations/it.json
index 2b573170e69087..044cd66e1430f4 100644
--- a/homeassistant/components/griddy/translations/it.json
+++ b/homeassistant/components/griddy/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Questa Zona di Carico \u00e8 gi\u00e0 configurata"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
"step": {
diff --git a/homeassistant/components/griddy/translations/no.json b/homeassistant/components/griddy/translations/no.json
index 000b5dae3067a6..b8d98aae0fd600 100644
--- a/homeassistant/components/griddy/translations/no.json
+++ b/homeassistant/components/griddy/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Denne Load Zone er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
"step": {
diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json
index e28b4b6f2e6ead..4fe11d213e82e9 100644
--- a/homeassistant/components/griddy/translations/pl.json
+++ b/homeassistant/components/griddy/translations/pl.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/griddy/translations/ru.json b/homeassistant/components/griddy/translations/ru.json
index 9fe98107510336..ee253e43f56bbe 100644
--- a/homeassistant/components/griddy/translations/ru.json
+++ b/homeassistant/components/griddy/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0439 \u0437\u043e\u043d\u044b \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/griddy/translations/zh-Hant.json b/homeassistant/components/griddy/translations/zh-Hant.json
index e112a2c682f8e2..0b8b5130772473 100644
--- a/homeassistant/components/griddy/translations/zh-Hant.json
+++ b/homeassistant/components/griddy/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u6b64\u8ca0\u8f09\u5340\u57df\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index 87eb2cd615b0fa..4a0050868d92ae 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -1,7 +1,8 @@
"""Provide the functionality to group entities."""
import asyncio
+from contextvars import ContextVar
import logging
-from typing import Any, Iterable, List, Optional, cast
+from typing import Any, Dict, Iterable, List, Optional, Set, cast
import voluptuous as vol
@@ -17,23 +18,17 @@
ENTITY_MATCH_NONE,
EVENT_HOMEASSISTANT_START,
SERVICE_RELOAD,
- STATE_CLOSED,
- STATE_HOME,
- STATE_LOCKED,
- STATE_NOT_HOME,
STATE_OFF,
- STATE_OK,
STATE_ON,
- STATE_OPEN,
- STATE_PROBLEM,
- STATE_UNKNOWN,
- STATE_UNLOCKED,
)
-from homeassistant.core import CoreState, callback
+from homeassistant.core import CoreState, callback, split_entity_id
import homeassistant.helpers.config_validation as cv
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
+from homeassistant.helpers.integration_platform import (
+ async_process_integration_platforms,
+)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
@@ -60,8 +55,12 @@
PLATFORMS = ["light", "cover", "notify"]
+REG_KEY = f"{DOMAIN}_registry"
+
_LOGGER = logging.getLogger(__name__)
+current_domain: ContextVar[str] = ContextVar("current_domain")
+
def _conf_preprocess(value):
"""Preprocess alternative configuration formats."""
@@ -87,35 +86,42 @@ def _conf_preprocess(value):
extra=vol.ALLOW_EXTRA,
)
-# List of ON/OFF state tuples for groupable states
-_GROUP_TYPES = [
- (STATE_ON, STATE_OFF),
- (STATE_HOME, STATE_NOT_HOME),
- (STATE_OPEN, STATE_CLOSED),
- (STATE_LOCKED, STATE_UNLOCKED),
- (STATE_PROBLEM, STATE_OK),
-]
+class GroupIntegrationRegistry:
+ """Class to hold a registry of integrations."""
+
+ on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF}
+ off_on_mapping: Dict[str, str] = {STATE_OFF: STATE_ON}
+ on_states_by_domain: Dict[str, Set] = {}
+ exclude_domains: Set = set()
-def _get_group_on_off(state):
- """Determine the group on/off states based on a state."""
- for states in _GROUP_TYPES:
- if state in states:
- return states
+ def exclude_domain(self) -> None:
+ """Exclude the current domain."""
+ self.exclude_domains.add(current_domain.get())
- return None, None
+ def on_off_states(self, on_states: Set, off_state: str) -> None:
+ """Register on and off states for the current domain."""
+ for on_state in on_states:
+ if on_state not in self.on_off_mapping:
+ self.on_off_mapping[on_state] = off_state
+
+ if len(on_states) == 1 and off_state not in self.off_on_mapping:
+ self.off_on_mapping[off_state] = list(on_states)[0]
+
+ self.on_states_by_domain[current_domain.get()] = set(on_states)
@bind_hass
def is_on(hass, entity_id):
"""Test if the group state is in its ON-state."""
- state = hass.states.get(entity_id)
+ if REG_KEY not in hass.data:
+ # Integration not setup yet, it cannot be on
+ return False
- if state:
- group_on, _ = _get_group_on_off(state.state)
+ state = hass.states.get(entity_id)
- # If we found a group_type, compare to ON-state
- return group_on is not None and state.state == group_on
+ if state is not None:
+ return state.state in hass.data[REG_KEY].on_off_mapping
return False
@@ -209,6 +215,10 @@ async def async_setup(hass, config):
if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
+ hass.data[REG_KEY] = GroupIntegrationRegistry()
+
+ await async_process_integration_platforms(hass, DOMAIN, _process_group_platform)
+
await _async_process_config(hass, config, component)
async def reload_service_handler(service):
@@ -332,6 +342,13 @@ async def groups_service_handler(service):
return True
+async def _process_group_platform(hass, domain, platform):
+ """Process a group platform."""
+
+ current_domain.set(domain)
+ platform.async_describe_on_off_states(hass, hass.data[REG_KEY])
+
+
async def _async_process_config(hass, config, component):
"""Process group configuration."""
hass.data.setdefault(GROUP_ORDER, 0)
@@ -414,14 +431,12 @@ def __init__(
"""
self.hass = hass
self._name = name
- self._state = STATE_UNKNOWN
+ self._state = None
self._icon = icon
- if entity_ids:
- self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
- else:
- self.tracking = ()
- self.group_on = None
- self.group_off = None
+ self._set_tracked(entity_ids)
+ self._on_off = None
+ self._assumed = None
+ self._on_states = None
self.user_defined = user_defined
self.mode = any
if mode:
@@ -492,7 +507,7 @@ async def async_create_group(
if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
- await component.async_add_entities([group], True)
+ await component.async_add_entities([group])
return group
@@ -532,6 +547,7 @@ def state_attributes(self):
data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order}
if not self.user_defined:
data[ATTR_AUTO] = True
+
return data
@property
@@ -550,25 +566,57 @@ async def async_update_tracked_entity_ids(self, entity_ids):
This method must be run in the event loop.
"""
- await self.async_stop()
- self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
- self.group_on, self.group_off = None, None
+ self._async_stop()
+ self._set_tracked(entity_ids)
+ self._reset_tracked_state()
+ self._async_start()
- await self.async_update_ha_state(True)
- self.async_start()
+ def _set_tracked(self, entity_ids):
+ """Tuple of entities to be tracked."""
+ # tracking are the entities we want to track
+ # trackable are the entities we actually watch
+
+ if not entity_ids:
+ self.tracking = ()
+ self.trackable = ()
+ return
+
+ excluded_domains = self.hass.data[REG_KEY].exclude_domains
+
+ tracking = []
+ trackable = []
+ for ent_id in entity_ids:
+ ent_id_lower = ent_id.lower()
+ domain = split_entity_id(ent_id_lower)[0]
+ tracking.append(ent_id_lower)
+ if domain not in excluded_domains:
+ trackable.append(ent_id_lower)
+
+ self.trackable = tuple(trackable)
+ self.tracking = tuple(tracking)
@callback
- def async_start(self):
+ def _async_start(self, *_):
+ """Start tracking members and write state."""
+ self._reset_tracked_state()
+ self._async_start_tracking()
+ self.async_write_ha_state()
+
+ @callback
+ def _async_start_tracking(self):
"""Start tracking members.
This method must be run in the event loop.
"""
- if self._async_unsub_state_changed is None:
+ if self.trackable and self._async_unsub_state_changed is None:
self._async_unsub_state_changed = async_track_state_change_event(
- self.hass, self.tracking, self._async_state_changed_listener
+ self.hass, self.trackable, self._async_state_changed_listener
)
- async def async_stop(self):
+ self._async_update_group_state()
+
+ @callback
+ def _async_stop(self):
"""Unregister the group from Home Assistant.
This method must be run in the event loop.
@@ -579,19 +627,24 @@ async def async_stop(self):
async def async_update(self):
"""Query all members and determine current group state."""
- self._state = STATE_UNKNOWN
+ self._state = None
self._async_update_group_state()
async def async_added_to_hass(self):
"""Handle addition to Home Assistant."""
+ if self.hass.state != CoreState.running:
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, self._async_start
+ )
+ return
+
if self.tracking:
- self.async_start()
+ self._reset_tracked_state()
+ self._async_start_tracking()
async def async_will_remove_from_hass(self):
"""Handle removal from Home Assistant."""
- if self._async_unsub_state_changed:
- self._async_unsub_state_changed()
- self._async_unsub_state_changed = None
+ self._async_stop()
async def _async_state_changed_listener(self, event):
"""Respond to a member state changing.
@@ -603,21 +656,47 @@ async def _async_state_changed_listener(self, event):
return
self.async_set_context(event.context)
- self._async_update_group_state(event.data.get("new_state"))
+ new_state = event.data.get("new_state")
+
+ if new_state is None:
+ # The state was removed from the state machine
+ self._reset_tracked_state()
+
+ self._async_update_group_state(new_state)
self.async_write_ha_state()
- @property
- def _tracking_states(self):
- """Return the states that the group is tracking."""
- states = []
+ def _reset_tracked_state(self):
+ """Reset tracked state."""
+ self._on_off = {}
+ self._assumed = {}
+ self._on_states = set()
- for entity_id in self.tracking:
+ for entity_id in self.trackable:
state = self.hass.states.get(entity_id)
if state is not None:
- states.append(state)
-
- return states
+ self._see_state(state)
+
+ def _see_state(self, new_state):
+ """Keep track of the the state."""
+ entity_id = new_state.entity_id
+ domain = new_state.domain
+ state = new_state.state
+ registry = self.hass.data[REG_KEY]
+ self._assumed[entity_id] = new_state.attributes.get(ATTR_ASSUMED_STATE)
+
+ if domain not in registry.on_states_by_domain:
+ # Handle the group of a group case
+ if state in registry.on_off_mapping:
+ self._on_states.add(state)
+ elif state in registry.off_on_mapping:
+ self._on_states.add(registry.off_on_mapping[state])
+ self._on_off[entity_id] = state in registry.on_off_mapping
+ else:
+ entity_on_state = registry.on_states_by_domain[domain]
+ if domain in self.hass.data[REG_KEY].on_states_by_domain:
+ self._on_states.update(entity_on_state)
+ self._on_off[entity_id] = state in entity_on_state
@callback
def _async_update_group_state(self, tr_state=None):
@@ -629,57 +708,39 @@ def _async_update_group_state(self, tr_state=None):
This method must be run in the event loop.
"""
# To store current states of group entities. Might not be needed.
- states = None
- gr_state = self._state
- gr_on = self.group_on
- gr_off = self.group_off
-
- # We have not determined type of group yet
- if gr_on is None:
- if tr_state is None:
- states = self._tracking_states
-
- for state in states:
- gr_on, gr_off = _get_group_on_off(state.state)
- if gr_on is not None:
- break
- else:
- gr_on, gr_off = _get_group_on_off(tr_state.state)
+ if tr_state:
+ self._see_state(tr_state)
- if gr_on is not None:
- self.group_on, self.group_off = gr_on, gr_off
-
- # We cannot determine state of the group
- if gr_on is None:
+ if not self._on_off:
return
- if tr_state is None or (
- (gr_state == gr_on and tr_state.state == gr_off)
- or (gr_state == gr_off and tr_state.state == gr_on)
- or tr_state.state not in (gr_on, gr_off)
- ):
- if states is None:
- states = self._tracking_states
-
- if self.mode(state.state == gr_on for state in states):
- self._state = gr_on
- else:
- self._state = gr_off
-
- elif tr_state.state in (gr_on, gr_off):
- self._state = tr_state.state
-
if (
tr_state is None
or self._assumed_state
and not tr_state.attributes.get(ATTR_ASSUMED_STATE)
):
- if states is None:
- states = self._tracking_states
-
- self._assumed_state = self.mode(
- state.attributes.get(ATTR_ASSUMED_STATE) for state in states
- )
+ self._assumed_state = self.mode(self._assumed.values())
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
+
+ num_on_states = len(self._on_states)
+ # If all the entity domains we are tracking
+ # have the same on state we use this state
+ # and its hass.data[REG_KEY].on_off_mapping to off
+ if num_on_states == 1:
+ on_state = list(self._on_states)[0]
+ # If we do not have an on state for any domains
+ # we use None (which will be STATE_UNKNOWN)
+ elif num_on_states == 0:
+ self._state = None
+ return
+ # If the entity domains have more than one
+ # on state, we use STATE_ON/STATE_OFF
+ else:
+ on_state = STATE_ON
+ group_is_on = self.mode(self._on_off.values())
+ if group_is_on:
+ self._state = on_state
+ else:
+ self._state = self.hass.data[REG_KEY].on_off_mapping[on_state]
diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py
index b0d0b8b7bd2480..ab2ad65713de9e 100644
--- a/homeassistant/components/group/cover.py
+++ b/homeassistant/components/group/cover.py
@@ -39,7 +39,7 @@
STATE_OPEN,
STATE_OPENING,
)
-from homeassistant.core import State
+from homeassistant.core import CoreState, State
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
@@ -162,6 +162,10 @@ async def async_added_to_hass(self):
self.hass, self._entities, self._update_supported_features_event
)
)
+
+ if self.hass.state == CoreState.running:
+ await self.async_update()
+ return
await super().async_added_to_hass()
@property
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index 289bb8df3f0962..007e05edfbb415 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -36,7 +36,7 @@
STATE_ON,
STATE_UNAVAILABLE,
)
-from homeassistant.core import State
+from homeassistant.core import CoreState, State
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -111,6 +111,11 @@ async def async_state_changed_listener(event):
self.hass, self._entity_ids, async_state_changed_listener
)
)
+
+ if self.hass.state == CoreState.running:
+ await self.async_update()
+ return
+
await super().async_added_to_hass()
@property
diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py
index 366596beb0bb90..e6ed422db0f33e 100644
--- a/homeassistant/components/growatt_server/sensor.py
+++ b/homeassistant/components/growatt_server/sensor.py
@@ -12,6 +12,7 @@
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
+ CURRENCY_EURO,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
@@ -21,6 +22,7 @@
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
FREQUENCY_HERTZ,
+ PERCENTAGE,
POWER_WATT,
TEMP_CELSIUS,
VOLT,
@@ -39,8 +41,8 @@
# Sensor type order is: Sensor name, Unit of measurement, api data name, additional options
TOTAL_SENSOR_TYPES = {
- "total_money_today": ("Total money today", "€", "plantMoneyText", {}),
- "total_money_total": ("Money lifetime", "€", "totalMoneyText", {}),
+ "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}),
+ "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}),
"total_energy_today": (
"Energy Today",
ENERGY_KILO_WATT_HOUR,
@@ -230,7 +232,7 @@
),
"storage_battery_percentage": (
"Battery percentage",
- "%",
+ PERCENTAGE,
"capacity",
{"device_class": DEVICE_CLASS_BATTERY},
),
@@ -338,7 +340,7 @@
),
"storage_load_percentage": (
"Load percentage",
- "%",
+ PERCENTAGE,
"loadPercent",
{"device_class": DEVICE_CLASS_BATTERY, "round": 2},
),
diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py
index c63d80163bc11e..7942dba361e69c 100644
--- a/homeassistant/components/guardian/binary_sensor.py
+++ b/homeassistant/components/guardian/binary_sensor.py
@@ -3,7 +3,11 @@
from aioguardian import Client
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_MOISTURE,
+ BinarySensorEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -22,8 +26,8 @@
SENSOR_KIND_AP_INFO = "ap_enabled"
SENSOR_KIND_LEAK_DETECTED = "leak_detected"
SENSORS = [
- (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", "connectivity"),
- (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", "moisture"),
+ (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", DEVICE_CLASS_CONNECTIVITY),
+ (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", DEVICE_CLASS_MOISTURE),
]
diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py
index 71ec271753ecb0..760cf960e4389a 100644
--- a/homeassistant/components/guardian/config_flow.py
+++ b/homeassistant/components/guardian/config_flow.py
@@ -83,7 +83,7 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, discovery_info):
"""Handle the configuration via zeroconf."""
if discovery_info is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"])
await self._async_set_unique_id(pin)
diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json
index 3f87d3260f4a62..47abe35037d1e7 100644
--- a/homeassistant/components/guardian/strings.json
+++ b/homeassistant/components/guardian/strings.json
@@ -5,8 +5,8 @@
"user": {
"description": "Configure a local Elexa Guardian device.",
"data": {
- "ip_address": "IP Address",
- "port": "Port"
+ "ip_address": "[%key:common::config_flow::data::ip%]",
+ "port": "[%key:common::config_flow::data::port%]"
}
},
"zeroconf_confirm": {
@@ -14,9 +14,9 @@
}
},
"abort": {
- "already_configured": "This Guardian device has already been configured.",
- "already_in_progress": "Guardian device configuration is already in process.",
- "connection_error": "Failed to connect to the Guardian device."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
}
diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json
index a94126753be454..207242e32c3133 100644
--- a/homeassistant/components/guardian/translations/ca.json
+++ b/homeassistant/components/guardian/translations/ca.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu Guardian ja est\u00e0 configurat.",
- "already_in_progress": "La configuraci\u00f3 del dispositiu Guardian ja est\u00e0 en curs.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu Guardian."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json
new file mode 100644
index 00000000000000..eb707f48797b1e
--- /dev/null
+++ b/homeassistant/components/guardian/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json
index 40a8a003c12422..2e47f4d28aa909 100644
--- a/homeassistant/components/guardian/translations/en.json
+++ b/homeassistant/components/guardian/translations/en.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "This Guardian device has already been configured.",
- "already_in_progress": "Guardian device configuration is already in process.",
+ "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 to the Guardian device."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json
index ec2a724e94432e..657261f213ec61 100644
--- a/homeassistant/components/guardian/translations/es.json
+++ b/homeassistant/components/guardian/translations/es.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Este dispositivo Guardian ya ha sido configurado",
"already_in_progress": "La configuraci\u00f3n del dispositivo Guardian ya est\u00e1 en proceso.",
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se ha podido conectar con el dispositivo Guardian."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json
new file mode 100644
index 00000000000000..fbbf9b86ceb5c8
--- /dev/null
+++ b/homeassistant/components/guardian/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "Guardian seadmega \u00fchenduse loomine nurjus."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json
index 52742b4e816f18..427c34e6ca13c4 100644
--- a/homeassistant/components/guardian/translations/fr.json
+++ b/homeassistant/components/guardian/translations/fr.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Ce p\u00e9riph\u00e9rique Guardian a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9.",
"already_in_progress": "La configuration de l'appareil Guardian est d\u00e9j\u00e0 en cours.",
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Guardian."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json
index 3f1999be761ad8..24647fff40e57d 100644
--- a/homeassistant/components/guardian/translations/it.json
+++ b/homeassistant/components/guardian/translations/it.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Questo dispositivo Guardian \u00e8 gi\u00e0 stato configurato",
- "already_in_progress": "La configurazione del dispositivo Guardian \u00e8 gi\u00e0 in corso.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo Guardian."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/lb.json b/homeassistant/components/guardian/translations/lb.json
index 1de8bb9baea92f..e76dd195c14668 100644
--- a/homeassistant/components/guardian/translations/lb.json
+++ b/homeassistant/components/guardian/translations/lb.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "D\u00ebse Guardian Apparat ass scho konfigur\u00e9iert.",
"already_in_progress": "Guardian Apparat Konfiguratioun ass schonn am gaang.",
+ "cannot_connect": "Feeler beim verbannen",
"connection_error": "Feeler beim verbannen mam Guardian Apparat."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json
index fbe5f881124d4b..7a6db75afee40e 100644
--- a/homeassistant/components/guardian/translations/no.json
+++ b/homeassistant/components/guardian/translations/no.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Denne Guardian-enheten er allerede konfigurert.",
- "already_in_progress": "Konfigurasjon av Guardian-enheter er allerede i gang.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kan ikke koble til Guardian-enheten."
},
"step": {
"user": {
"data": {
"ip_address": "IP adresse",
- "port": ""
+ "port": "Port"
},
"description": "Konfigurer en lokal Elexa Guardian-enhet."
},
diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json
index 22706a1babcb60..d3770d8fada7be 100644
--- a/homeassistant/components/guardian/translations/pl.json
+++ b/homeassistant/components/guardian/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Guardian, spr\u00f3buj ponownie."
},
"step": {
@@ -9,8 +9,13 @@
"data": {
"ip_address": "Adres IP",
"port": "Port"
- }
+ },
+ "description": "Skonfiguruj lokalne urz\u0105dzenie Elexa Guardian."
+ },
+ "zeroconf_confirm": {
+ "description": "Czy chcesz skonfigurowa\u0107 to urz\u0105dzenie Guardian?"
}
}
- }
+ },
+ "title": "Elexa Guardian"
}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json
index c9fe3b07ff7a6f..36eefd1a4cd2ea 100644
--- a/homeassistant/components/guardian/translations/ru.json
+++ b/homeassistant/components/guardian/translations/ru.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
+ "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 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"step": {
diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json
index d91c0c3ba8cfb8..1b0ca0090d9343 100644
--- a/homeassistant/components/guardian/translations/zh-Hant.json
+++ b/homeassistant/components/guardian/translations/zh-Hant.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Guardian \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "Guardian \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "Guardian \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"step": {
diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json
index 80eed48cde9f49..a2605124dc40fa 100644
--- a/homeassistant/components/hangouts/manifest.json
+++ b/homeassistant/components/hangouts/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hangouts",
"requirements": [
- "hangups==0.4.10"
+ "hangups==0.4.11"
],
"codeowners": []
}
diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json
index 0133bf421ff041..bed46e823d93f7 100644
--- a/homeassistant/components/hangouts/strings.json
+++ b/homeassistant/components/hangouts/strings.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts is already configured",
- "unknown": "Unknown error occurred."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"invalid_login": "Invalid Login, please try again.",
@@ -26,4 +26,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json
index 7daa1b4f671ea1..2d1d81bb08db66 100644
--- a/homeassistant/components/hangouts/translations/ca.json
+++ b/homeassistant/components/hangouts/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts ja est\u00e0 configurat",
- "unknown": "S'ha produ\u00eft un error desconegut."
+ "already_configured": "El servei ja est\u00e0 configurat",
+ "unknown": "Error inesperat"
},
"error": {
"invalid_2fa": "La verificaci\u00f3 en dos passos no \u00e9s v\u00e0lida, torna-ho a provar.",
diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json
index 0c2c91d1ac8c15..5de8ac249706f6 100644
--- a/homeassistant/components/hangouts/translations/en.json
+++ b/homeassistant/components/hangouts/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts is already configured",
- "unknown": "Unknown error occurred."
+ "already_configured": "Service is already configured",
+ "unknown": "Unexpected error"
},
"error": {
"invalid_2fa": "Invalid 2 Factor Authentication, please try again.",
diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json
index b1c29f3577b26d..e8293aff79f7f4 100644
--- a/homeassistant/components/hangouts/translations/et.json
+++ b/homeassistant/components/hangouts/translations/et.json
@@ -1,6 +1,8 @@
{
"config": {
"error": {
+ "invalid_2fa": "Vale 2-teguriline autentimine, proovige uuesti.",
+ "invalid_2fa_method": "Kehtetu 2FA meetod (kontrollige telefoni teel).",
"invalid_login": "Vale Kasutajanimi, palun proovige uuesti."
},
"step": {
diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json
index 29ddab2491318d..4831d51ef12c2b 100644
--- a/homeassistant/components/hangouts/translations/it.json
+++ b/homeassistant/components/hangouts/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts \u00e8 gi\u00e0 configurato",
- "unknown": "Si \u00e8 verificato un errore sconosciuto."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
+ "unknown": "Errore imprevisto"
},
"error": {
"invalid_2fa": "Autenticazione a 2 fattori non valida, riprovare.",
diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json
index 8818898c0b4c17..ffb0d91a2f1f7f 100644
--- a/homeassistant/components/hangouts/translations/no.json
+++ b/homeassistant/components/hangouts/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts er allerede konfigurert",
- "unknown": "Ukjent feil oppstod."
+ "already_configured": "Tjenesten er allerede konfigurert",
+ "unknown": "Uventet feil"
},
"error": {
"invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.",
diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json
index 69c3020bbfb0d7..2dd3364bd53ae5 100644
--- a/homeassistant/components/hangouts/translations/pl.json
+++ b/homeassistant/components/hangouts/translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Google Hangouts jest ju\u017c skonfigurowany.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"invalid_2fa": "Nieprawid\u0142owe uwierzytelnienie dwusk\u0142adnikowe, spr\u00f3buj ponownie.",
diff --git a/homeassistant/components/hangouts/translations/ru.json b/homeassistant/components/hangouts/translations/ru.json
index 580c858d15f175..d352258ba33e4e 100644
--- a/homeassistant/components/hangouts/translations/ru.json
+++ b/homeassistant/components/hangouts/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ "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.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
diff --git a/homeassistant/components/hangouts/translations/zh-Hant.json b/homeassistant/components/hangouts/translations/zh-Hant.json
index 1619eaddb63810..62a220eaa9462d 100644
--- a/homeassistant/components/hangouts/translations/zh-Hant.json
+++ b/homeassistant/components/hangouts/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts \u5df2\u7d93\u8a2d\u5b9a",
- "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
"invalid_2fa": "\u96d9\u91cd\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json
index 86de34672be0e1..f953639e22df3e 100644
--- a/homeassistant/components/harmony/strings.json
+++ b/homeassistant/components/harmony/strings.json
@@ -15,11 +15,11 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json
index 5bb279c0482d28..8111fe3b3446be 100644
--- a/homeassistant/components/harmony/translations/ca.json
+++ b/homeassistant/components/harmony/translations/ca.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_configured_device": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/en.json b/homeassistant/components/harmony/translations/en.json
index ce13e79e2799a3..8843d741154f5f 100644
--- a/homeassistant/components/harmony/translations/en.json
+++ b/homeassistant/components/harmony/translations/en.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Device is already configured",
+ "already_configured_device": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/es.json b/homeassistant/components/harmony/translations/es.json
index 39305d30680bb0..ffd165e5d7ade5 100644
--- a/homeassistant/components/harmony/translations/es.json
+++ b/homeassistant/components/harmony/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "El dispositivo ya est\u00e1 configurado"
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "already_configured_device": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo",
diff --git a/homeassistant/components/harmony/translations/et.json b/homeassistant/components/harmony/translations/et.json
new file mode 100644
index 00000000000000..3a91142ca7a407
--- /dev/null
+++ b/homeassistant/components/harmony/translations/et.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Seade on juba h\u00e4\u00e4letatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/harmony/translations/fr.json b/homeassistant/components/harmony/translations/fr.json
index 4343ec3139dd16..ed838ed0115d48 100644
--- a/homeassistant/components/harmony/translations/fr.json
+++ b/homeassistant/components/harmony/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",
+ "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
diff --git a/homeassistant/components/harmony/translations/it.json b/homeassistant/components/harmony/translations/it.json
index c658e69e0c05c1..ea28a617ff3a7f 100644
--- a/homeassistant/components/harmony/translations/it.json
+++ b/homeassistant/components/harmony/translations/it.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/no.json b/homeassistant/components/harmony/translations/no.json
index c3518792851546..d93948dab0a12f 100644
--- a/homeassistant/components/harmony/translations/no.json
+++ b/homeassistant/components/harmony/translations/no.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_configured_device": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json
index 12bbcfaca1825e..664a849061ae00 100644
--- a/homeassistant/components/harmony/translations/pl.json
+++ b/homeassistant/components/harmony/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "Logitech Harmony Hub {name}",
"step": {
diff --git a/homeassistant/components/harmony/translations/ru.json b/homeassistant/components/harmony/translations/ru.json
index 4e995a26c48ff5..e7ff5241a1252c 100644
--- a/homeassistant/components/harmony/translations/ru.json
+++ b/homeassistant/components/harmony/translations/ru.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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_configured_device": "\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json
index dfd1249d629b41..01af1b23af7253 100644
--- a/homeassistant/components/harmony/translations/zh-Hant.json
+++ b/homeassistant/components/harmony/translations/zh-Hant.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"flow_title": "\u7f85\u6280 Harmony Hub {name}",
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 69c53225d49bce..9604507fcf07c0 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -127,18 +127,57 @@
@bind_hass
-async def async_get_addon_info(hass: HomeAssistantType, addon_id: str) -> dict:
+async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict:
"""Return add-on info.
- The addon_id is a snakecased concatenation of the 'repository' value
- found in the add-on info and the 'slug' value found in the add-on config.json.
- In the add-on info the addon_id is called 'slug'.
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ return await hassio.get_addon_info(slug)
+
+
+@bind_hass
+async def async_install_addon(hass: HomeAssistantType, slug: str) -> None:
+ """Install add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ command = f"/addons/{slug}/install"
+ await hassio.send_command(command)
+
+
+@bind_hass
+async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> None:
+ """Uninstall add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ command = f"/addons/{slug}/uninstall"
+ await hassio.send_command(command)
+
+
+@bind_hass
+async def async_start_addon(hass: HomeAssistantType, slug: str) -> None:
+ """Start add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ command = f"/addons/{slug}/start"
+ await hassio.send_command(command)
+
+
+@bind_hass
+async def async_stop_addon(hass: HomeAssistantType, slug: str) -> None:
+ """Stop add-on.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
- result = await hassio.get_addon_info(addon_id)
- return result["data"]
+ command = f"/addons/{slug}/stop"
+ await hassio.send_command(command)
@callback
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
index be2cec5ae9c645..95f861e609766c 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -12,11 +12,14 @@
import async_timeout
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
+from homeassistant.components.onboarding import async_is_onboarded
+from homeassistant.const import HTTP_UNAUTHORIZED
from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO
_LOGGER = logging.getLogger(__name__)
+MAX_UPLOAD_SIZE = 1024 * 1024 * 1024
NO_TIMEOUT = re.compile(
r"^(?:"
@@ -31,6 +34,10 @@
r")$"
)
+NO_AUTH_ONBOARDING = re.compile(
+ r"^(?:" r"|supervisor/logs" r"|snapshots/[^/]+/.+" r")$"
+)
+
NO_AUTH = re.compile(
r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$"
)
@@ -52,8 +59,9 @@ async def _handle(
self, request: web.Request, path: str
) -> Union[web.Response, web.StreamResponse]:
"""Route data to Hass.io."""
- if _need_auth(path) and not request[KEY_AUTHENTICATED]:
- return web.Response(status=401)
+ hass = request.app["hass"]
+ if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]:
+ return web.Response(status=HTTP_UNAUTHORIZED)
return await self._command_proxy(path, request)
@@ -70,6 +78,16 @@ async def _command_proxy(
read_timeout = _get_timeout(path)
data = None
headers = _init_header(request)
+ if path == "snapshots/new/upload":
+ # We need to reuse the full content type that includes the boundary
+ headers[
+ "Content-Type"
+ ] = request._stored_content_type # pylint: disable=protected-access
+
+ # Snapshots are big, so we need to adjust the allowed size
+ request._client_max_size = ( # pylint: disable=protected-access
+ MAX_UPLOAD_SIZE
+ )
try:
with async_timeout.timeout(10):
@@ -133,8 +151,10 @@ def _get_timeout(path: str) -> int:
return 300
-def _need_auth(path: str) -> bool:
+def _need_auth(hass, path: str) -> bool:
"""Return if a path need authentication."""
+ if not async_is_onboarded(hass) and NO_AUTH_ONBOARDING.match(path):
+ return False
if NO_AUTH.match(path):
return False
return True
diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json
index c3817d25776dae..4c307582281157 100644
--- a/homeassistant/components/hdmi_cec/manifest.json
+++ b/homeassistant/components/hdmi_cec/manifest.json
@@ -1,8 +1,7 @@
{
- "disabled": "Dependency contains code that breaks Home Assistant.",
"domain": "hdmi_cec",
"name": "HDMI-CEC",
"documentation": "https://www.home-assistant.io/integrations/hdmi_cec",
- "requirements": ["pyCEC==0.4.13"],
- "codeowners": []
+ "requirements": ["pyCEC==0.4.14"],
+ "codeowners": ["@newAM"]
}
diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py
index 138d1c4462c7b5..efffb93e97859e 100644
--- a/homeassistant/components/heos/config_flow.py
+++ b/homeassistant/components/heos/config_flow.py
@@ -32,7 +32,7 @@ async def async_step_ssdp(self, discovery_info):
self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = hostname
# Abort if other flows in progress or an entry already exists
if self._async_in_progress() or self._async_current_entries():
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
await self.async_set_unique_id(DOMAIN)
# Show selection form
return self.async_show_form(step_id="user")
@@ -50,7 +50,7 @@ async def async_step_user(self, user_input=None):
self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {})
# Only a single entry is needed for all devices
if self._async_current_entries():
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
# Try connecting to host if provided
errors = {}
host = None
@@ -64,7 +64,7 @@ async def async_step_user(self, user_input=None):
self.hass.data.pop(DATA_DISCOVERED_HOSTS)
return await self.async_step_import({CONF_HOST: host})
except HeosError:
- errors[CONF_HOST] = "connection_failure"
+ errors[CONF_HOST] = "cannot_connect"
finally:
await heos.disconnect()
diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json
index b633b3c82b6117..09ada292afde62 100644
--- a/homeassistant/components/heos/strings.json
+++ b/homeassistant/components/heos/strings.json
@@ -10,10 +10,10 @@
}
},
"error": {
- "connection_failure": "Unable to connect to the specified host."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_setup": "You can only configure a single Heos connection as it will support all devices on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/heos/translations/ca.json b/homeassistant/components/heos/translations/ca.json
index d8bcf494615bdd..f12e18ac99909b 100644
--- a/homeassistant/components/heos/translations/ca.json
+++ b/homeassistant/components/heos/translations/ca.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 de Heos tot i que aquesta ja pot controlar tots els dispositius de la xarxa."
+ "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 de Heos tot i que aquesta ja pot controlar tots els dispositius de la xarxa.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_failure": "No s'ha pogut connectar amb l'amfitri\u00f3 especificat."
},
"step": {
diff --git a/homeassistant/components/heos/translations/en.json b/homeassistant/components/heos/translations/en.json
index be84c08fa5ef51..ed82022c969b5f 100644
--- a/homeassistant/components/heos/translations/en.json
+++ b/homeassistant/components/heos/translations/en.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "You can only configure a single Heos connection as it will support all devices on the network."
+ "already_setup": "You can only configure a single Heos connection as it will support all devices on the network.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_failure": "Unable to connect to the specified host."
},
"step": {
diff --git a/homeassistant/components/heos/translations/it.json b/homeassistant/components/heos/translations/it.json
index 81350edf9adf50..353d0a7e3193a8 100644
--- a/homeassistant/components/heos/translations/it.json
+++ b/homeassistant/components/heos/translations/it.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "\u00c8 possibile configurare una singola connessione Heos poich\u00e9 supporta tutti i dispositivi sulla rete."
+ "already_setup": "\u00c8 possibile configurare una singola connessione Heos poich\u00e9 supporta tutti i dispositivi sulla rete.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_failure": "Impossibile connettersi all'host specificato."
},
"step": {
diff --git a/homeassistant/components/heos/translations/pl.json b/homeassistant/components/heos/translations/pl.json
index 5394d57bb74098..55a05356e9da22 100644
--- a/homeassistant/components/heos/translations/pl.json
+++ b/homeassistant/components/heos/translations/pl.json
@@ -4,6 +4,7 @@
"already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno po\u0142\u0105czenie Heos, poniewa\u017c b\u0119dzie ono obs\u0142ugiwa\u0107 wszystkie urz\u0105dzenia w sieci."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_failure": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z okre\u015blonym hostem."
},
"step": {
diff --git a/homeassistant/components/heos/translations/ru.json b/homeassistant/components/heos/translations/ru.json
index b92ea253ebee20..2ee40afbe0c1d3 100644
--- a/homeassistant/components/heos/translations/ru.json
+++ b/homeassistant/components/heos/translations/ru.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438."
+ "already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443."
},
"step": {
diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py
index 779afa10cca643..359f966d119174 100644
--- a/homeassistant/components/hikvision/binary_sensor.py
+++ b/homeassistant/components/hikvision/binary_sensor.py
@@ -5,7 +5,12 @@
from pyhik.hikvision import HikCamera
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_MOTION,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import (
ATTR_LAST_TRIP_TIME,
CONF_CUSTOMIZE,
@@ -34,28 +39,28 @@
ATTR_DELAY = "delay"
DEVICE_CLASS_MAP = {
- "Motion": "motion",
- "Line Crossing": "motion",
- "Field Detection": "motion",
+ "Motion": DEVICE_CLASS_MOTION,
+ "Line Crossing": DEVICE_CLASS_MOTION,
+ "Field Detection": DEVICE_CLASS_MOTION,
"Video Loss": None,
- "Tamper Detection": "motion",
+ "Tamper Detection": DEVICE_CLASS_MOTION,
"Shelter Alarm": None,
"Disk Full": None,
"Disk Error": None,
- "Net Interface Broken": "connectivity",
- "IP Conflict": "connectivity",
+ "Net Interface Broken": DEVICE_CLASS_CONNECTIVITY,
+ "IP Conflict": DEVICE_CLASS_CONNECTIVITY,
"Illegal Access": None,
"Video Mismatch": None,
"Bad Video": None,
- "PIR Alarm": "motion",
- "Face Detection": "motion",
- "Scene Change Detection": "motion",
+ "PIR Alarm": DEVICE_CLASS_MOTION,
+ "Face Detection": DEVICE_CLASS_MOTION,
+ "Scene Change Detection": DEVICE_CLASS_MOTION,
"I/O": None,
- "Unattended Baggage": "motion",
- "Attended Baggage": "motion",
+ "Unattended Baggage": DEVICE_CLASS_MOTION,
+ "Attended Baggage": DEVICE_CLASS_MOTION,
"Recording Failure": None,
- "Exiting Region": "motion",
- "Entering Region": "motion",
+ "Exiting Region": DEVICE_CLASS_MOTION,
+ "Entering Region": DEVICE_CLASS_MOTION,
}
CUSTOMIZE_SCHEMA = vol.Schema(
@@ -250,8 +255,7 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
- attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update()
+ attr = {ATTR_LAST_TRIP_TIME: self._sensor_last_update()}
if self._delay != 0:
attr[ATTR_DELAY] = self._delay
diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py
index 2e924135bd402d..2f1f89cd26159c 100644
--- a/homeassistant/components/hikvisioncam/switch.py
+++ b/homeassistant/components/hikvisioncam/switch.py
@@ -68,11 +68,6 @@ def __init__(self, name, hikvision_cam):
self._hikvision_cam = hikvision_cam
self._state = STATE_OFF
- @property
- def should_poll(self):
- """Poll for status regularly."""
- return True
-
@property
def name(self):
"""Return the name of the device if any."""
diff --git a/homeassistant/components/hisense_aehw4a1/strings.json b/homeassistant/components/hisense_aehw4a1/strings.json
index 5d9b6f1ef96136..58e6b057c12a5b 100644
--- a/homeassistant/components/hisense_aehw4a1/strings.json
+++ b/homeassistant/components/hisense_aehw4a1/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible.",
- "no_devices_found": "No Hisense AEH-W4A1 devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/hisense_aehw4a1/translations/ca.json b/homeassistant/components/hisense_aehw4a1/translations/ca.json
index 3a05c1d6123482..4ead6779df66b4 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/ca.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'ha trobat cap dispositiu AEH-W4A1 a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 del AEH-W4A1 de Hisense."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/en.json b/homeassistant/components/hisense_aehw4a1/translations/en.json
index 4b2926002495cc..abdf245c8549f1 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/en.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No Hisense AEH-W4A1 devices found on the network.",
- "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/it.json b/homeassistant/components/hisense_aehw4a1/translations/it.json
index 0da43b27a82646..b6951f4d6478b1 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/it.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo Hisense AEH-W4A1 trovato sulla rete.",
- "single_instance_allowed": "\u00c8 consentita solo una configurazione di Hisense AEH-W4A1"
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/no.json b/homeassistant/components/hisense_aehw4a1/translations/no.json
index 6a8d6cbe443323..9be1a735d51c33 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/no.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen Hisense AEH-W4A1-enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Bare en enkelt konfigurasjon av Hisense AEH-W4A1 er mulig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/ru.json b/homeassistant/components/hisense_aehw4a1/translations/ru.json
index 0d982c13eb0862..e26dcfb42c0ad7 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/ru.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index 0e6fabb66aa462..79c60572ba5a23 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -8,7 +8,7 @@
from typing import Optional, cast
from aiohttp import web
-from sqlalchemy import and_, bindparam, func
+from sqlalchemy import and_, bindparam, func, not_, or_
from sqlalchemy.ext import baked
import voluptuous as vol
@@ -29,6 +29,10 @@
)
from homeassistant.core import Context, State, split_entity_id
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entityfilter import (
+ CONF_ENTITY_GLOBS,
+ INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
+)
import homeassistant.util.dt as dt_util
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -41,27 +45,20 @@
STATE_KEY = "state"
LAST_CHANGED_KEY = "last_changed"
-# Not reusing from entityfilter because history does not support glob filtering
-_FILTER_SCHEMA_INNER = vol.Schema(
- {
- vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
- }
-)
-_FILTER_SCHEMA = vol.Schema(
+GLOB_TO_SQL_CHARS = {
+ 42: "%", # *
+ 46: "_", # .
+}
+
+CONFIG_SCHEMA = vol.Schema(
{
- vol.Optional(
- CONF_INCLUDE, default=_FILTER_SCHEMA_INNER({})
- ): _FILTER_SCHEMA_INNER,
- vol.Optional(
- CONF_EXCLUDE, default=_FILTER_SCHEMA_INNER({})
- ): _FILTER_SCHEMA_INNER,
- vol.Optional(CONF_ORDER, default=False): cv.boolean,
- }
+ DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
+ {vol.Optional(CONF_ORDER, default=False): cv.boolean}
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
)
-CONFIG_SCHEMA = vol.Schema({DOMAIN: _FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA)
-
SIGNIFICANT_DOMAINS = (
"climate",
"device_tracker",
@@ -130,8 +127,14 @@ def _get_significant_states(
else:
baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time"))
- if filters:
- filters.bake(baked_query, entity_ids)
+ 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))
+ if filters:
+ filters.bake(baked_query)
if end_time is not None:
baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time"))
@@ -299,10 +302,14 @@ def _get_states_with_session(
query = query.join(
most_recent_state_ids,
States.state_id == most_recent_state_ids.c.max_state_id,
- ).filter(~States.domain.in_(IGNORE_DOMAINS))
+ )
- if filters:
- query = filters.apply(query, entity_ids)
+ if entity_ids is not None:
+ query = query.filter(States.entity_id.in_(entity_ids))
+ else:
+ query = query.filter(~States.domain.in_(IGNORE_DOMAINS))
+ if filters:
+ query = filters.apply(query)
return [LazyState(row) for row in execute(query)]
@@ -542,7 +549,7 @@ def _sorted_significant_states_json(
# Optionally reorder the result to respect the ordering given
# by any entities explicitly included in the configuration.
- if self.use_include_order:
+ if self.filters and self.use_include_order:
sorted_result = []
for order_entity in self.filters.included_entities:
for state_list in result:
@@ -563,11 +570,14 @@ def sqlalchemy_filter_from_include_exclude_conf(conf):
if exclude:
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
+ filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, [])
include = conf.get(CONF_INCLUDE)
if include:
filters.included_entities = include.get(CONF_ENTITIES, [])
filters.included_domains = include.get(CONF_DOMAINS, [])
- return filters
+ filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, [])
+
+ return filters if filters.has_config else None
class Filters:
@@ -577,94 +587,77 @@ def __init__(self):
"""Initialise the include and exclude filters."""
self.excluded_entities = []
self.excluded_domains = []
+ self.excluded_entity_globs = []
+
self.included_entities = []
self.included_domains = []
+ self.included_entity_globs = []
- def apply(self, query, entity_ids=None):
- """Apply the include/exclude filter on domains and entities on query.
+ def apply(self, query):
+ """Apply the entity filter."""
+ if not self.has_config:
+ return query
- Following rules apply:
- * only the include section is configured - just query the specified
- entities or domains.
- * only the exclude section is configured - filter the specified
- entities and domains from all the entities in the system.
- * if include and exclude is defined - select the entities specified in
- the include and filter out the ones from the exclude list.
- """
- # specific entities requested - do not in/exclude anything
- if entity_ids is not None:
- return query.filter(States.entity_id.in_(entity_ids))
-
- query = query.filter(~States.domain.in_(IGNORE_DOMAINS))
+ return query.filter(self.entity_filter())
- entity_filter = self.entity_filter()
- if entity_filter is not None:
- query = query.filter(entity_filter)
+ @property
+ def has_config(self):
+ """Determine if there is any filter configuration."""
+ if (
+ self.excluded_entities
+ or self.excluded_domains
+ or self.excluded_entity_globs
+ or self.included_entities
+ or self.included_domains
+ or self.included_entity_globs
+ ):
+ return True
- return query
+ return False
- def bake(self, baked_query, entity_ids=None):
+ def bake(self, baked_query):
"""Update a baked query.
Works the same as apply on a baked_query.
"""
- if entity_ids is not None:
- baked_query += lambda q: q.filter(
- States.entity_id.in_(bindparam("entity_ids", expanding=True))
- )
+ if not self.has_config:
return
- baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS))
-
- if (
- self.excluded_entities
- or self.excluded_domains
- or self.included_entities
- or self.included_domains
- ):
- baked_query += lambda q: q.filter(self.entity_filter())
+ baked_query += lambda q: q.filter(self.entity_filter())
def entity_filter(self):
"""Generate the entity filter query."""
- entity_filter = None
- # filter if only excluded domain is configured
- if self.excluded_domains and not self.included_domains:
- entity_filter = ~States.domain.in_(self.excluded_domains)
- if self.included_entities:
- entity_filter &= States.entity_id.in_(self.included_entities)
- # filter if only included domain is configured
- elif not self.excluded_domains and self.included_domains:
- entity_filter = States.domain.in_(self.included_domains)
- if self.included_entities:
- entity_filter |= States.entity_id.in_(self.included_entities)
- # filter if included and excluded domain is configured
- elif self.excluded_domains and self.included_domains:
- entity_filter = ~States.domain.in_(self.excluded_domains)
- if self.included_entities:
- entity_filter &= States.domain.in_(
- self.included_domains
- ) | States.entity_id.in_(self.included_entities)
- else:
- entity_filter &= States.domain.in_(
- self.included_domains
- ) & ~States.domain.in_(self.excluded_domains)
- # no domain filter just included entities
- elif (
- not self.excluded_domains
- and not self.included_domains
- and self.included_entities
- ):
- entity_filter = States.entity_id.in_(self.included_entities)
- # finally apply excluded entities filter if configured
+ includes = []
+ if self.included_domains:
+ includes.append(States.domain.in_(self.included_domains))
+ if self.included_entities:
+ includes.append(States.entity_id.in_(self.included_entities))
+ for glob in self.included_entity_globs:
+ includes.append(_glob_to_like(glob))
+
+ excludes = []
+ if self.excluded_domains:
+ excludes.append(States.domain.in_(self.excluded_domains))
if self.excluded_entities:
- if entity_filter is not None:
- entity_filter = (entity_filter) & ~States.entity_id.in_(
- self.excluded_entities
- )
- else:
- entity_filter = ~States.entity_id.in_(self.excluded_entities)
+ excludes.append(States.entity_id.in_(self.excluded_entities))
+ for glob in self.excluded_entity_globs:
+ excludes.append(_glob_to_like(glob))
+
+ if not includes and not excludes:
+ return None
+
+ if includes and not excludes:
+ return or_(*includes)
+
+ if not excludes and includes:
+ return not_(or_(*excludes))
+
+ return or_(*includes) & not_(or_(*excludes))
+
- return entity_filter
+def _glob_to_like(glob_str):
+ """Translate glob to sql."""
+ return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS))
class LazyState(State):
diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py
index 8b993b0e837ba8..40c0a76bfba884 100644
--- a/homeassistant/components/history_stats/sensor.py
+++ b/homeassistant/components/history_stats/sensor.py
@@ -174,11 +174,6 @@ def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py
index 27c648f554bdd4..120148a8f813f4 100644
--- a/homeassistant/components/hive/binary_sensor.py
+++ b/homeassistant/components/hive/binary_sensor.py
@@ -1,9 +1,16 @@
"""Support for the Hive binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OPENING,
+ BinarySensorEntity,
+)
from . import DATA_HIVE, DOMAIN, HiveEntity
-DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"}
+DEVICETYPE_DEVICE_CLASS = {
+ "motionsensor": DEVICE_CLASS_MOTION,
+ "contactsensor": DEVICE_CLASS_OPENING,
+}
def setup_platform(hass, config, add_entities, discovery_info=None):
diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json
index 060a1a0a200d6b..f8fb9bc8c2a181 100644
--- a/homeassistant/components/hive/manifest.json
+++ b/homeassistant/components/hive/manifest.json
@@ -2,6 +2,6 @@
"domain": "hive",
"name": "Hive",
"documentation": "https://www.home-assistant.io/integrations/hive",
- "requirements": ["pyhiveapi==0.2.20.1"],
+ "requirements": ["pyhiveapi==0.2.20.2"],
"codeowners": ["@Rendili", "@KJonline"]
}
diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json
new file mode 100644
index 00000000000000..6f39806287630f
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/hu.json b/homeassistant/components/hlk_sw16/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/nl.json b/homeassistant/components/hlk_sw16/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/pl.json b/homeassistant/components/hlk_sw16/translations/pl.json
new file mode 100644
index 00000000000000..25dab56796cc15
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json
index 798fe2930a03e1..79455783edf7e9 100644
--- a/homeassistant/components/home_connect/strings.json
+++ b/homeassistant/components/home_connect/strings.json
@@ -2,15 +2,15 @@
"config": {
"step": {
"pick_implementation": {
- "title": "Pick Authentication Method"
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
- "missing_configuration": "The Home Connect component is not configured. Please follow the documentation.",
+ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
- "default": "Successfully authenticated with Home Connect."
+ "default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}
diff --git a/homeassistant/components/home_connect/translations/ca.json b/homeassistant/components/home_connect/translations/ca.json
index 6553ce7e24db2b..12f761904fcc9d 100644
--- a/homeassistant/components/home_connect/translations/ca.json
+++ b/homeassistant/components/home_connect/translations/ca.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "El component Home Connect no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
+ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
"no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})"
},
"create_entry": {
- "default": "Autenticaci\u00f3 exitosa amb Home Connect."
+ "default": "Autenticaci\u00f3 exitosa"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/en.json b/homeassistant/components/home_connect/translations/en.json
index 24190814216b1d..d44b51a0b9787e 100644
--- a/homeassistant/components/home_connect/translations/en.json
+++ b/homeassistant/components/home_connect/translations/en.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "The Home Connect component is not configured. Please follow the documentation.",
+ "missing_configuration": "The component is not configured. Please follow the documentation.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
},
"create_entry": {
- "default": "Successfully authenticated with Home Connect."
+ "default": "Successfully authenticated"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json
index 7457f7487d4ffd..8c60c994df0c74 100644
--- a/homeassistant/components/home_connect/translations/es.json
+++ b/homeassistant/components/home_connect/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n."
+ "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"create_entry": {
"default": "Autenticado correctamente con Home Assistant."
diff --git a/homeassistant/components/home_connect/translations/fr.json b/homeassistant/components/home_connect/translations/fr.json
index 630960b1c916ee..42a0c34fe8182b 100644
--- a/homeassistant/components/home_connect/translations/fr.json
+++ b/homeassistant/components/home_connect/translations/fr.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Le composant Home Connect n'est pas configur\u00e9. Veuillez suivre la documentation."
+ "missing_configuration": "Le composant Home Connect 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} )"
},
"create_entry": {
"default": "Authentification r\u00e9ussie avec Home Connect."
diff --git a/homeassistant/components/home_connect/translations/it.json b/homeassistant/components/home_connect/translations/it.json
index 98aa955b0204ac..3dc834bfd85c74 100644
--- a/homeassistant/components/home_connect/translations/it.json
+++ b/homeassistant/components/home_connect/translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "Il componente Home Connect non \u00e8 configurato. Si prega di seguire la documentazione.",
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
"no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
},
"create_entry": {
- "default": "Autenticazione riuscita con Home Connect."
+ "default": "Autenticazione riuscita"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json
index 973e1a0ec8803f..8d1f5554e7f880 100644
--- a/homeassistant/components/home_connect/translations/ko.json
+++ b/homeassistant/components/home_connect/translations/ko.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"create_entry": {
"default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
diff --git a/homeassistant/components/home_connect/translations/lb.json b/homeassistant/components/home_connect/translations/lb.json
index 1820e1e2788ab3..210c871a1b8e73 100644
--- a/homeassistant/components/home_connect/translations/lb.json
+++ b/homeassistant/components/home_connect/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connecz Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun."
+ "missing_configuration": "Home Connecz Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Home Connect authentifiz\u00e9iert."
diff --git a/homeassistant/components/home_connect/translations/no.json b/homeassistant/components/home_connect/translations/no.json
index 908f62efbc9399..69d84fdf653a32 100644
--- a/homeassistant/components/home_connect/translations/no.json
+++ b/homeassistant/components/home_connect/translations/no.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )"
},
"create_entry": {
- "default": "Vellykket godkjenning med Home Connect"
+ "default": "Vellykket godkjenning"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/zh-Hant.json b/homeassistant/components/home_connect/translations/zh-Hant.json
index 5132dedd515c35..56f8fd16874bf0 100644
--- a/homeassistant/components/home_connect/translations/zh-Hant.json
+++ b/homeassistant/components/home_connect/translations/zh-Hant.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"create_entry": {
- "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Home Connect \u8a2d\u5099\u3002"
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py
index 7e4a04333449a5..1ff3915f1215c9 100644
--- a/homeassistant/components/homeassistant/scene.py
+++ b/homeassistant/components/homeassistant/scene.py
@@ -35,15 +35,15 @@ def _convert_states(states):
"""Convert state definitions to State objects."""
result = {}
- for entity_id in states:
+ for entity_id, info in states.items():
entity_id = cv.entity_id(entity_id)
- if isinstance(states[entity_id], dict):
- entity_attrs = states[entity_id].copy()
+ if isinstance(info, dict):
+ entity_attrs = info.copy()
state = entity_attrs.pop(ATTR_STATE, None)
attributes = entity_attrs
else:
- state = states[entity_id]
+ state = info
attributes = {}
# YAML translates 'on' to a boolean
diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py
index 800d6bb5f7740a..4498702bac4a14 100644
--- a/homeassistant/components/homeassistant/triggers/event.py
+++ b/homeassistant/components/homeassistant/triggers/event.py
@@ -11,6 +11,7 @@
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data"
+CONF_EVENT_CONTEXT = "context"
_LOGGER = logging.getLogger(__name__)
@@ -19,36 +20,42 @@
vol.Required(CONF_PLATFORM): "event",
vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict,
+ vol.Optional(CONF_EVENT_CONTEXT): dict,
}
)
+def _populate_schema(config, config_parameter):
+ if config_parameter not in config:
+ return None
+
+ return vol.Schema(
+ {vol.Required(key): value for key, value in config[config_parameter].items()},
+ extra=vol.ALLOW_EXTRA,
+ )
+
+
async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="event"
):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
- event_data_schema = None
- if config.get(CONF_EVENT_DATA):
- event_data_schema = vol.Schema(
- {
- vol.Required(key): value
- for key, value in config.get(CONF_EVENT_DATA).items()
- },
- extra=vol.ALLOW_EXTRA,
- )
+ event_data_schema = _populate_schema(config, CONF_EVENT_DATA)
+ event_context_schema = _populate_schema(config, CONF_EVENT_CONTEXT)
@callback
def handle_event(event):
"""Listen for events and calls the action when data matches."""
- if event_data_schema:
- # Check that the event data matches the configured
+ try:
+ # Check that the event data and context match the configured
# schema if one was provided
- try:
+ if event_data_schema:
event_data_schema(event.data)
- except vol.Invalid:
- # If event data doesn't match requested schema, skip event
- return
+ if event_context_schema:
+ event_context_schema(event.context.as_dict())
+ except vol.Invalid:
+ # If event doesn't match, skip event
+ return
hass.async_run_job(
action,
diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py
index f57db0ed56a9cc..915856951d2122 100644
--- a/homeassistant/components/homeassistant/triggers/state.py
+++ b/homeassistant/components/homeassistant/triggers/state.py
@@ -1,7 +1,7 @@
"""Offer state listening automation rules."""
from datetime import timedelta
import logging
-from typing import Dict, Optional
+from typing import Any, Dict, Optional
import voluptuous as vol
@@ -25,18 +25,43 @@
CONF_FROM = "from"
CONF_TO = "to"
-TRIGGER_SCHEMA = vol.Schema(
+BASE_SCHEMA = {
+ vol.Required(CONF_PLATFORM): "state",
+ vol.Required(CONF_ENTITY_ID): cv.entity_ids,
+ vol.Optional(CONF_FOR): cv.positive_time_period_template,
+ vol.Optional(CONF_ATTRIBUTE): cv.match_all,
+}
+
+TRIGGER_STATE_SCHEMA = vol.Schema(
{
- vol.Required(CONF_PLATFORM): "state",
- vol.Required(CONF_ENTITY_ID): cv.entity_ids,
+ **BASE_SCHEMA,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): vol.Any(str, [str]),
vol.Optional(CONF_TO): vol.Any(str, [str]),
- vol.Optional(CONF_FOR): cv.positive_time_period_template,
- vol.Optional(CONF_ATTRIBUTE): cv.match_all,
}
)
+TRIGGER_ATTRIBUTE_SCHEMA = vol.Schema(
+ {
+ **BASE_SCHEMA,
+ vol.Optional(CONF_FROM): cv.match_all,
+ vol.Optional(CONF_TO): cv.match_all,
+ }
+)
+
+
+def TRIGGER_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
+ """Validate trigger."""
+ if not isinstance(value, dict):
+ raise vol.Invalid("Expected a dictionary")
+
+ # We use this approach instead of vol.Any because
+ # this gives better error messages.
+ if CONF_ATTRIBUTE in value:
+ return TRIGGER_ATTRIBUTE_SCHEMA(value)
+
+ return TRIGGER_STATE_SCHEMA(value)
+
async def async_attach_trigger(
hass: HomeAssistant,
@@ -145,7 +170,7 @@ def _check_same_state(_, _2, new_st: State):
else:
cur_value = new_st.attributes.get(attribute)
- if CONF_TO not in config:
+ if CONF_FROM in config and CONF_TO not in config:
return cur_value != old_value
return cur_value == new_value
diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py
index adacc939870aa1..5f03fb593d623a 100644
--- a/homeassistant/components/homeassistant/triggers/time_pattern.py
+++ b/homeassistant/components/homeassistant/triggers/time_pattern.py
@@ -36,7 +36,7 @@ def __call__(self, value):
if isinstance(value, str) and value.startswith("/"):
number = int(value[1:])
else:
- number = int(value)
+ value = number = int(value)
if not (0 <= number <= self.maximum):
raise vol.Invalid(f"must be a value between 0 and {self.maximum}")
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 2fb61a61fedf3d..6dc2e2364b63e3 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -9,7 +9,11 @@
from pyhap.const import CATEGORY_OTHER
from homeassistant.components import cover, vacuum
-from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
+from homeassistant.components.cover import (
+ DEVICE_CLASS_GARAGE,
+ DEVICE_CLASS_GATE,
+ DEVICE_CLASS_WINDOW,
+)
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
@@ -24,6 +28,7 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
STATE_ON,
STATE_UNAVAILABLE,
@@ -154,6 +159,11 @@ def get_accessory(hass, driver, state, aid, config):
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
):
a_type = "GarageDoorOpener"
+ elif (
+ device_class == DEVICE_CLASS_WINDOW
+ and features & cover.SUPPORT_SET_POSITION
+ ):
+ a_type = "Window"
elif features & cover.SUPPORT_SET_POSITION:
a_type = "WindowCovering"
elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
@@ -197,7 +207,7 @@ def get_accessory(hass, driver, state, aid, config):
a_type = "CarbonMonoxideSensor"
elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id:
a_type = "CarbonDioxideSensor"
- elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"):
+ elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", LIGHT_LUX):
a_type = "LightSensor"
elif state.domain == "switch":
diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py
index 6a3206ac41bd4a..3d35b685271f76 100644
--- a/homeassistant/components/homekit/config_flow.py
+++ b/homeassistant/components/homekit/config_flow.py
@@ -118,7 +118,7 @@ async def async_step_user(self, user_input=None):
self.entry_title = title
return await self.async_step_pairing()
- default_domains = [] if self._async_current_entries() else DEFAULT_DOMAINS
+ default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS
setup_schema = vol.Schema(
{
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool,
@@ -146,17 +146,27 @@ async def _async_available_port(self):
find_next_available_port, DEFAULT_CONFIG_FLOW_PORT
)
+ @callback
+ def _async_current_names(self):
+ """Return a set of bridge names."""
+ current_entries = self._async_current_entries()
+
+ return {
+ entry.data[CONF_NAME]
+ for entry in current_entries
+ if CONF_NAME in entry.data
+ }
+
@callback
def _async_available_name(self):
"""Return an available for the bridge."""
- current_entries = self._async_current_entries()
# We always pick a RANDOM name to avoid Zeroconf
# name collisions. If the name has been seen before
# pairing will probably fail.
acceptable_chars = string.ascii_uppercase + string.digits
trailer = "".join(random.choices(acceptable_chars, k=4))
- all_names = {entry.data[CONF_NAME] for entry in current_entries}
+ all_names = self._async_current_names()
suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}"
while suggested_name in all_names:
trailer = "".join(random.choices(acceptable_chars, k=4))
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index d8eec057191e2d..9a2bc37a5a91b4 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -136,6 +136,7 @@
SERV_TEMPERATURE_SENSOR = "TemperatureSensor"
SERV_THERMOSTAT = "Thermostat"
SERV_VALVE = "Valve"
+SERV_WINDOW = "Window"
SERV_WINDOW_COVERING = "WindowCovering"
# #### Characteristics ####
diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json
index 3c6162e378eaaa..4bac36a846728f 100644
--- a/homeassistant/components/homekit/translations/fr.json
+++ b/homeassistant/components/homekit/translations/fr.json
@@ -32,6 +32,7 @@
"data": {
"camera_copy": "Cam\u00e9ras prenant en charge les flux H.264 natifs"
},
+ "description": "V\u00e9rifiez toutes les cam\u00e9ras prenant en charge les flux H.264 natifs. Si la cam\u00e9ra ne produit pas de flux H.264, le syst\u00e8me transcodera la vid\u00e9o en H.264 pour HomeKit. Le transcodage n\u00e9cessite un processeur performant et il est peu probable qu'il fonctionne sur des ordinateurs \u00e0 carte unique.",
"title": "S\u00e9lectionnez le codec vid\u00e9o de la cam\u00e9ra."
},
"exclude": {
diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py
index 1e18ad82b94ddb..d8d8da5a974bea 100644
--- a/homeassistant/components/homekit/type_covers.py
+++ b/homeassistant/components/homekit/type_covers.py
@@ -1,7 +1,11 @@
"""Class to hold all cover accessories."""
import logging
-from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING
+from pyhap.const import (
+ CATEGORY_GARAGE_DOOR_OPENER,
+ CATEGORY_WINDOW,
+ CATEGORY_WINDOW_COVERING,
+)
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION,
@@ -46,6 +50,7 @@
HK_POSITION_GOING_TO_MIN,
HK_POSITION_STOPPED,
SERV_GARAGE_DOOR_OPENER,
+ SERV_WINDOW,
SERV_WINDOW_COVERING,
)
@@ -128,16 +133,16 @@ def async_update_state(self, new_state):
self.char_current_state.set_value(current_door_state)
-class WindowCoveringBase(HomeAccessory):
+class OpeningDeviceBase(HomeAccessory):
"""Generate a base Window accessory for a cover entity.
This class is used for WindowCoveringBasic and
WindowCovering
"""
- def __init__(self, *args, category):
- """Initialize a WindowCoveringBase accessory object."""
- super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
+ def __init__(self, *args, category, service):
+ """Initialize a OpeningDeviceBase accessory object."""
+ super().__init__(*args, category=category)
state = self.hass.states.get(self.entity_id)
self.features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@@ -151,7 +156,7 @@ def __init__(self, *args, category):
if self._supports_tilt:
self.chars.extend([CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE])
- self.serv_cover = self.add_preload_service(SERV_WINDOW_COVERING, self.chars)
+ self.serv_cover = self.add_preload_service(service, self.chars)
if self._supports_stop:
self.char_hold_position = self.serv_cover.configure_char(
@@ -211,16 +216,15 @@ def async_update_state(self, new_state):
self._homekit_target_tilt = None
-@TYPES.register("WindowCovering")
-class WindowCovering(WindowCoveringBase, HomeAccessory):
- """Generate a Window accessory for a cover entity.
+class OpeningDevice(OpeningDeviceBase, HomeAccessory):
+ """Generate a Window/WindowOpening accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
- def __init__(self, *args):
+ def __init__(self, *args, category, service):
"""Initialize a WindowCovering accessory object."""
- super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
+ super().__init__(*args, category=category, service=service)
state = self.hass.states.get(self.entity_id)
self._homekit_target = None
@@ -278,8 +282,34 @@ def async_update_state(self, new_state):
super().async_update_state(new_state)
+@TYPES.register("Window")
+class Window(OpeningDevice):
+ """Generate a Window accessory for a cover entity with DEVICE_CLASS_WINDOW.
+
+ The entity must support: set_cover_position.
+ """
+
+ def __init__(self, *args):
+ """Initialize a Window accessory object."""
+ super().__init__(*args, category=CATEGORY_WINDOW, service=SERV_WINDOW)
+
+
+@TYPES.register("WindowCovering")
+class WindowCovering(OpeningDevice):
+ """Generate a WindowCovering accessory for a cover entity.
+
+ The entity must support: set_cover_position.
+ """
+
+ def __init__(self, *args):
+ """Initialize a WindowCovering accessory object."""
+ super().__init__(
+ *args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING
+ )
+
+
@TYPES.register("WindowCoveringBasic")
-class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
+class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
@@ -287,8 +317,10 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
"""
def __init__(self, *args):
- """Initialize a WindowCovering accessory object."""
- super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
+ """Initialize a WindowCoveringBasic accessory object."""
+ super().__init__(
+ *args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING
+ )
state = self.hass.states.get(self.entity_id)
self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index 612d8e53a02238..086934ea6f756f 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -24,6 +24,10 @@
STATE_ON,
)
from homeassistant.core import callback
+from homeassistant.util.color import (
+ color_temperature_mired_to_kelvin,
+ color_temperature_to_hs,
+)
from .accessories import TYPES, HomeAccessory
from .const import (
@@ -64,8 +68,6 @@ def __init__(self, *args):
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
- self._hue = None
- self._saturation = None
elif self._features & SUPPORT_COLOR_TEMP:
# ColorTemperature and Hue characteristic should not be
# exposed both. Both states are tracked separately in HomeKit,
@@ -179,7 +181,16 @@ def async_update_state(self, new_state):
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
- hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None))
+ if ATTR_HS_COLOR in new_state.attributes:
+ hue, saturation = new_state.attributes[ATTR_HS_COLOR]
+ elif ATTR_COLOR_TEMP in new_state.attributes:
+ hue, saturation = color_temperature_to_hs(
+ color_temperature_mired_to_kelvin(
+ new_state.attributes[ATTR_COLOR_TEMP]
+ )
+ )
+ else:
+ hue, saturation = None, None
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
hue = round(hue, 0)
saturation = round(saturation, 0)
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index 7d8dcac046d70d..feae1b5cd0676b 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -2,11 +2,19 @@
import logging
from pyhap.const import CATEGORY_ALARM_SYSTEM
+from pyhap.loader import get_loader
from homeassistant.components.alarm_control_panel import DOMAIN
+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.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
@@ -36,6 +44,13 @@
STATE_ALARM_TRIGGERED: 4,
}
+HASS_TO_HOMEKIT_SERVICES = {
+ SERVICE_ALARM_ARM_HOME: 0,
+ SERVICE_ALARM_ARM_AWAY: 1,
+ SERVICE_ALARM_ARM_NIGHT: 2,
+ SERVICE_ALARM_DISARM: 3,
+}
+
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
STATE_TO_SERVICE = {
@@ -56,13 +71,72 @@ def __init__(self, *args):
state = self.hass.states.get(self.entity_id)
self._alarm_code = self.config.get(ATTR_CODE)
+ supported_states = state.attributes.get(
+ ATTR_SUPPORTED_FEATURES,
+ (
+ SUPPORT_ALARM_ARM_HOME
+ | SUPPORT_ALARM_ARM_AWAY
+ | SUPPORT_ALARM_ARM_NIGHT
+ | SUPPORT_ALARM_TRIGGER
+ ),
+ )
+
+ loader = get_loader()
+ default_current_states = loader.get_char(
+ "SecuritySystemCurrentState"
+ ).properties.get("ValidValues")
+ default_target_services = loader.get_char(
+ "SecuritySystemTargetState"
+ ).properties.get("ValidValues")
+
+ current_supported_states = [
+ HASS_TO_HOMEKIT[STATE_ALARM_DISARMED],
+ HASS_TO_HOMEKIT[STATE_ALARM_TRIGGERED],
+ ]
+ target_supported_services = [HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM]]
+
+ if supported_states & SUPPORT_ALARM_ARM_HOME:
+ current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_HOME])
+ target_supported_services.append(
+ HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_HOME]
+ )
+
+ if supported_states & SUPPORT_ALARM_ARM_AWAY:
+ current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_AWAY])
+ target_supported_services.append(
+ HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_AWAY]
+ )
+
+ if supported_states & SUPPORT_ALARM_ARM_NIGHT:
+ current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_NIGHT])
+ target_supported_services.append(
+ HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_NIGHT]
+ )
+
+ new_current_states = {
+ key: val
+ for key, val in default_current_states.items()
+ if val in current_supported_states
+ }
+ new_target_services = {
+ key: val
+ for key, val in default_target_services.items()
+ if val in target_supported_services
+ }
+
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm.configure_char(
- CHAR_CURRENT_SECURITY_STATE, value=3
+ CHAR_CURRENT_SECURITY_STATE,
+ value=HASS_TO_HOMEKIT[STATE_ALARM_DISARMED],
+ valid_values=new_current_states,
)
self.char_target_state = serv_alarm.configure_char(
- CHAR_TARGET_SECURITY_STATE, value=3, setter_callback=self.set_security_state
+ CHAR_TARGET_SECURITY_STATE,
+ value=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM],
+ valid_values=new_target_services,
+ setter_callback=self.set_security_state,
)
+
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.async_update_state(state)
diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py
index 0b8f0b3b2f84f2..333eb462439b54 100644
--- a/homeassistant/components/homekit_controller/alarm_control_panel.py
+++ b/homeassistant/components/homekit_controller/alarm_control_panel.py
@@ -110,10 +110,8 @@ async def set_alarm_state(self, state, code=None):
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
- attributes = {}
-
battery_level = self.service.value(CharacteristicsTypes.BATTERY_LEVEL)
- if battery_level:
- attributes[ATTR_BATTERY_LEVEL] = battery_level
- return attributes
+ if not battery_level:
+ return {}
+ return {ATTR_BATTERY_LEVEL: battery_level}
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 2c69512db9d681..9881ef15dcb02b 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -8,19 +8,44 @@
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.core import callback
+from homeassistant.helpers.device_registry import (
+ CONNECTION_NETWORK_MAC,
+ async_get_registry as async_get_device_registry,
+)
from .connection import get_accessory_name, get_bridge_information
from .const import DOMAIN, KNOWN_DEVICES
-HOMEKIT_IGNORE = ["Home Assistant Bridge"]
HOMEKIT_DIR = ".homekit"
+HOMEKIT_BRIDGE_DOMAIN = "homekit"
+HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge"
+HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge"
+
PAIRING_FILE = "pairing.json"
+MDNS_SUFFIX = "._hap._tcp.local."
+
PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$")
_LOGGER = logging.getLogger(__name__)
+DISALLOWED_CODES = {
+ "00000000",
+ "11111111",
+ "22222222",
+ "33333333",
+ "44444444",
+ "55555555",
+ "66666666",
+ "77777777",
+ "88888888",
+ "99999999",
+ "12345678",
+ "87654321",
+}
+
+
def normalize_hkid(hkid):
"""Normalize a hkid so that it is safe to compare with other normalized hkids."""
return hkid.lower()
@@ -42,9 +67,12 @@ def ensure_pin_format(pin):
If incorrect code is entered, an exception is raised.
"""
- match = PIN_FORMAT.search(pin)
+ match = PIN_FORMAT.search(pin.strip())
if not match:
raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
+ pin_without_dashes = "".join(match.groups())
+ if pin_without_dashes in DISALLOWED_CODES:
+ raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
return "-".join(match.groups())
@@ -59,6 +87,7 @@ def __init__(self):
"""Initialize the homekit_controller flow."""
self.model = None
self.hkid = None
+ self.name = None
self.devices = {}
self.controller = None
self.finish_pairing = None
@@ -76,9 +105,11 @@ async def async_step_user(self, user_input=None):
key = user_input["device"]
self.hkid = self.devices[key].device_id
self.model = self.devices[key].info["md"]
+ self.name = key[: -len(MDNS_SUFFIX)] if key.endswith(MDNS_SUFFIX) else key
await self.async_set_unique_id(
normalize_hkid(self.hkid), raise_on_progress=False
)
+
return await self.async_step_pair()
if self.controller is None:
@@ -141,6 +172,17 @@ async def async_step_unignore(self, user_input):
return self.async_abort(reason="no_devices")
+ async def _hkid_is_homekit_bridge(self, hkid):
+ """Determine if the device is a homekit bridge."""
+ dev_reg = await async_get_device_registry(self.hass)
+ device = dev_reg.async_get_device(
+ identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)}
+ )
+
+ if device is None:
+ return False
+ return device.model == HOMEKIT_BRIDGE_MODEL
+
async def async_step_zeroconf(self, discovery_info):
"""Handle a discovered HomeKit accessory.
@@ -153,6 +195,12 @@ async def async_step_zeroconf(self, discovery_info):
key.lower(): value for (key, value) in discovery_info["properties"].items()
}
+ if "id" not in properties:
+ _LOGGER.warning(
+ "HomeKit device %s: id not exposed, in violation of spec", properties
+ )
+ return self.async_abort(reason="invalid_properties")
+
# The hkid is a unique random number that looks like a pairing code.
# It changes if a device is factory reset.
hkid = properties["id"]
@@ -198,7 +246,6 @@ async def async_step_zeroconf(self, discovery_info):
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["hkid"] = hkid
- self.context["title_placeholders"] = {"name": name}
if paired:
# Device is paired but not to us - ignore it
@@ -208,9 +255,10 @@ async def async_step_zeroconf(self, discovery_info):
# Devices in HOMEKIT_IGNORE have native local integrations - users
# should be encouraged to use native integration and not confused
# by alternative HK API.
- if model in HOMEKIT_IGNORE:
+ if await self._hkid_is_homekit_bridge(hkid):
return self.async_abort(reason="ignored_model")
+ self.name = name
self.model = model
self.hkid = hkid
@@ -280,9 +328,8 @@ async def async_step_pair(self, pair_info=None):
# Its possible that the first try may have been busy so
# we always check to see if self.finish_paring has been
# set.
- discovery = await self.controller.find_ip_by_device_id(self.hkid)
-
try:
+ discovery = await self.controller.find_ip_by_device_id(self.hkid)
self.finish_pairing = await discovery.start_pairing(self.hkid)
except aiohomekit.BusyError:
@@ -332,9 +379,14 @@ async def async_step_protocol_error(self, user_input=None):
@callback
def _async_step_pair_show_form(self, errors=None):
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ placeholders = {"name": self.name}
+ self.context["title_placeholders"] = {"name": self.name}
+
return self.async_show_form(
step_id="pair",
errors=errors or {},
+ description_placeholders=placeholders,
data_schema=vol.Schema(
{vol.Required("pairing_code"): vol.All(str, vol.Strip)}
),
diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py
index 086f780b8162b7..5f43b3e8564e68 100644
--- a/homeassistant/components/homekit_controller/cover.py
+++ b/homeassistant/components/homekit_controller/cover.py
@@ -117,15 +117,12 @@ async def set_door_state(self, state):
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
- attributes = {}
-
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED
)
- if obstruction_detected:
- attributes["obstruction-detected"] = obstruction_detected
-
- return attributes
+ if not obstruction_detected:
+ return {}
+ return {"obstruction-detected": obstruction_detected}
class HomeKitWindowCover(HomeKitEntity, CoverEntity):
@@ -249,12 +246,9 @@ async def async_set_cover_tilt_position(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
- attributes = {}
-
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED
)
- if obstruction_detected:
- attributes["obstruction-detected"] = obstruction_detected
-
- return attributes
+ if not obstruction_detected:
+ return {}
+ return {"obstruction-detected": obstruction_detected}
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index 06199e9f2102a8..1fb4c05c595d86 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": [
- "aiohomekit==0.2.49"
+ "aiohomekit==0.2.53"
],
"zeroconf": [
"_hap._tcp.local."
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index 944729d2e5cfd7..2075eb9dcc38a8 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -7,6 +7,7 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
)
@@ -19,8 +20,6 @@
BRIGHTNESS_ICON = "mdi:brightness-6"
CO2_ICON = "mdi:molecule-co2"
-UNIT_LUX = "lux"
-
class HomeKitHumiditySensor(HomeKitEntity):
"""Representation of a Homekit humidity sensor."""
@@ -113,7 +112,7 @@ def icon(self):
@property
def unit_of_measurement(self):
"""Return units for the sensor."""
- return UNIT_LUX
+ return LIGHT_LUX
@property
def state(self):
diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json
index babfac05718138..3c6b8405e31e67 100644
--- a/homeassistant/components/homekit_controller/strings.json
+++ b/homeassistant/components/homekit_controller/strings.json
@@ -1,18 +1,18 @@
{
"title": "HomeKit Controller",
"config": {
- "flow_title": "HomeKit Accessory: {name}",
+ "flow_title": "{name} via HomeKit Accessory Protocol",
"step": {
"user": {
- "title": "Pair with HomeKit Accessory",
- "description": "Select the device you want to pair with",
+ "title": "Device selection",
+ "description": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Select the device you want to pair with:",
"data": {
"device": "Device"
}
},
"pair": {
- "title": "Pair with HomeKit Accessory",
- "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory",
+ "title": "Pair with a device via HomeKit Accessory Protocol",
+ "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
"data": {
"pairing_code": "Pairing Code"
}
@@ -44,27 +44,28 @@
"already_configured": "Accessory is already configured with this controller.",
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
- "already_in_progress": "Config flow for device is already in progress."
+ "invalid_properties": "Invalid properties announced by device.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
}
},
"device_automation": {
"trigger_type": {
- "single_press": "\"{subtype}\" pressed",
- "double_press": "\"{subtype}\" pressed twice",
- "long_press": "\"{subtype}\" pressed and held"
+ "single_press": "\"{subtype}\" pressed",
+ "double_press": "\"{subtype}\" pressed twice",
+ "long_press": "\"{subtype}\" pressed and held"
},
"trigger_subtype": {
- "doorbell": "Doorbell",
- "button1": "Button 1",
- "button2": "Button 2",
- "button3": "Button 3",
- "button4": "Button 4",
- "button5": "Button 5",
- "button6": "Button 6",
- "button7": "Button 7",
- "button8": "Button 8",
- "button9": "Button 9",
- "button10": "Button 10"
+ "doorbell": "Doorbell",
+ "button1": "Button 1",
+ "button2": "Button 2",
+ "button3": "Button 3",
+ "button4": "Button 4",
+ "button5": "Button 5",
+ "button6": "Button 6",
+ "button7": "Button 7",
+ "button8": "Button 8",
+ "button9": "Button 9",
+ "button10": "Button 10"
}
}
}
diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json
index bf46d58491701c..8b1dd8eca18699 100644
--- a/homeassistant/components/homekit_controller/translations/ca.json
+++ b/homeassistant/components/homekit_controller/translations/ca.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "No s'ha pogut vincular, no s'ha trobat el dispositiu.",
"already_configured": "Accessori ja configurat amb aquest controlador.",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.",
"ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.",
"invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2 ja hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.",
+ "invalid_properties": "Propietats anunciades pel dispositiu no v\u00e0lides.",
"no_devices": "No s'han trobat dispositius desvinculats."
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.",
"unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida."
},
- "flow_title": "Accessori HomeKit: {name}",
+ "flow_title": "{name} a trav\u00e9s de HomeKit Accessory Protocol",
"step": {
"busy_error": {
"description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "Codi de vinculaci\u00f3"
},
- "description": "Introdueix el codi de vinculaci\u00f3 de HomeKit per utilitzar aquest accessori (format XXX-XX-XXX)",
- "title": "Vinculaci\u00f3 amb"
+ "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.",
+ "title": "Vinculaci\u00f3 amb un dispositiu a trav\u00e9s de HomeKit Accessory Protocol"
},
"protocol_error": {
"description": "\u00c9s possible que el dispositiu no estigui en mode de vinculaci\u00f3, potser cal pr\u00e9mer un bot\u00f3 f\u00edsic o virtual. Assegura't que el dispositiu est\u00e0 en mode vinculaci\u00f3 o prova de reiniciar-lo, despr\u00e9s, segueix amb la vinculaci\u00f3.",
@@ -48,10 +49,30 @@
"data": {
"device": "Dispositiu"
},
- "description": "Selecciona el dispositiu amb el qual et vols vincular",
- "title": "Vinculaci\u00f3 amb un accessori HomeKit"
+ "description": "El controlador HomeKit es comunica a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Selecciona el dispositiu amb el qual et vols vincular:",
+ "title": "Selecci\u00f3 de dispositiu"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Bot\u00f3 1",
+ "button10": "Bot\u00f3 10",
+ "button2": "Bot\u00f3 2",
+ "button3": "Bot\u00f3 3",
+ "button4": "Bot\u00f3 4",
+ "button5": "Bot\u00f3 5",
+ "button6": "Bot\u00f3 6",
+ "button7": "Bot\u00f3 7",
+ "button8": "Bot\u00f3 8",
+ "button9": "Bot\u00f3 9",
+ "doorbell": "Timbre"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" premut dues vegades",
+ "long_press": "\"{subtype}\" premut i mantingut",
+ "single_press": "\"{subtype}\" premut"
+ }
+ },
"title": "Controlador HomeKit"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json
index b10fb6efe45fc6..6ee03a37f88fb5 100644
--- a/homeassistant/components/homekit_controller/translations/de.json
+++ b/homeassistant/components/homekit_controller/translations/de.json
@@ -36,5 +36,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Knopf 1",
+ "button10": "Knopf 10",
+ "button2": "Knopf 2",
+ "button3": "Knopf 3",
+ "button4": "Knopf 4",
+ "button5": "Knopf 5",
+ "button6": "Knopf 6",
+ "button7": "Knopf 7",
+ "button8": "Knopf 8",
+ "button9": "Knopf 9",
+ "doorbell": "T\u00fcrklingel"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" zweimal gedr\u00fcckt",
+ "long_press": "\"{subtype}\" gedr\u00fcckt und gehalten",
+ "single_press": "\"{subtype}\" gedr\u00fcckt"
+ }
+ },
"title": "HomeKit-Controller"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/el.json b/homeassistant/components/homekit_controller/translations/el.json
new file mode 100644
index 00000000000000..41ab3bf67042b9
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/el.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "invalid_properties": "\u0391\u03bd\u03b1\u03ba\u03bf\u03b9\u03bd\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae."
+ },
+ "step": {
+ "busy_error": {
+ "description": "\u039c\u03b1\u03c4\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03b5\u03cd\u03be\u03b7 \u03c3\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ad\u03c2 \u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7."
+ },
+ "max_tries_error": {
+ "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ac\u03b2\u03b5\u03b9 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc 100 \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 1",
+ "button10": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 10",
+ "button2": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 2",
+ "button3": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 3",
+ "button4": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 4",
+ "button5": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 5",
+ "button6": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 6",
+ "button7": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 7",
+ "button8": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 8",
+ "button9": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 9",
+ "doorbell": "\u039a\u03bf\u03c5\u03b4\u03bf\u03cd\u03bd\u03b9"
+ },
+ "trigger_type": {
+ "double_press": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b4\u03cd\u03bf \u03c6\u03bf\u03c1\u03ad\u03c2 \u03c4\u03bf \" {subtype} \"",
+ "long_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03b9 \u03ba\u03c1\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \" {subtype} \"",
+ "single_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \" {subtype} \""
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json
index afa790dd222ddd..3d7e237c73311b 100644
--- a/homeassistant/components/homekit_controller/translations/en.json
+++ b/homeassistant/components/homekit_controller/translations/en.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
"already_configured": "Accessory is already configured with this controller.",
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.",
"ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.",
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
+ "invalid_properties": "Invalid properties announced by device.",
"no_devices": "No unpaired devices could be found"
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "Unable to pair, please try again.",
"unknown_error": "Device reported an unknown error. Pairing failed."
},
- "flow_title": "HomeKit Accessory: {name}",
+ "flow_title": "{name} via HomeKit Accessory Protocol",
"step": {
"busy_error": {
"description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "Pairing Code"
},
- "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory",
- "title": "Pair with HomeKit Accessory"
+ "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
+ "title": "Pair with a device via HomeKit Accessory Protocol"
},
"protocol_error": {
"description": "The device may not be in pairing mode and may require a physical or virtual button press. Ensure the device is in pairing mode or try restarting the device, then continue to resume pairing.",
@@ -48,31 +49,30 @@
"data": {
"device": "Device"
},
- "description": "Select the device you want to pair with",
- "title": "Pair with HomeKit Accessory"
+ "description": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Select the device you want to pair with:",
+ "title": "Device selection"
}
}
},
- "title": "HomeKit Controller",
"device_automation": {
- "trigger_type": {
- "single_press": "\"{subtype}\" pressed",
- "double_press": "\"{subtype}\" pressed twice",
- "long_press": "\"{subtype}\" pressed and held"
- },
- "trigger_subtype": {
- "doorbell": "Doorbell",
- "button1": "Button 1",
- "button2": "Button 2",
- "button3": "Button 3",
- "button4": "Button 4",
- "button5": "Button 5",
- "button6": "Button 6",
- "button7": "Button 7",
- "button8": "Button 8",
- "button9": "Button 9",
- "button10": "Button 10"
- }
- }
-}
-
+ "trigger_subtype": {
+ "button1": "Button 1",
+ "button10": "Button 10",
+ "button2": "Button 2",
+ "button3": "Button 3",
+ "button4": "Button 4",
+ "button5": "Button 5",
+ "button6": "Button 6",
+ "button7": "Button 7",
+ "button8": "Button 8",
+ "button9": "Button 9",
+ "doorbell": "Doorbell"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" pressed twice",
+ "long_press": "\"{subtype}\" pressed and held",
+ "single_press": "\"{subtype}\" pressed"
+ }
+ },
+ "title": "HomeKit Controller"
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json
index 8eb450e65581f4..b1daa4e50cc8af 100644
--- a/homeassistant/components/homekit_controller/translations/es.json
+++ b/homeassistant/components/homekit_controller/translations/es.json
@@ -7,6 +7,7 @@
"already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.",
"ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.",
"invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.",
+ "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.",
"no_devices": "No se encontraron dispositivos no emparejados"
},
"error": {
@@ -53,5 +54,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Bot\u00f3n 1",
+ "button10": "Bot\u00f3n 10",
+ "button2": "Bot\u00f3n 2",
+ "button3": "Bot\u00f3n 3",
+ "button4": "Bot\u00f3n 4",
+ "button5": "Bot\u00f3n 5",
+ "button6": "Bot\u00f3n 6",
+ "button7": "Bot\u00f3n 7",
+ "button8": "Bot\u00f3n 8",
+ "button9": "Bot\u00f3n 9",
+ "doorbell": "Timbre de la puerta"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" pulsado dos veces",
+ "long_press": "\"{subtype}\" pulsado y mantenido",
+ "single_press": "\"{subtype}\" pulsado"
+ }
+ },
"title": "Accesorio HomeKit"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json
new file mode 100644
index 00000000000000..31788215005fc1
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Nupp 1",
+ "button10": "Nupp 10",
+ "button2": "Nupp 2",
+ "button3": "Nupp 3",
+ "button4": "Nupp 4",
+ "button5": "Nupp 5",
+ "button6": "Nupp 6",
+ "button7": "Nupp 7",
+ "button8": "Nupp 8",
+ "button9": "Nupp 9",
+ "doorbell": "Uksekell"
+ },
+ "trigger_type": {
+ "double_press": "\" {subtype} \" tehtud topeltkl\u00f5ps",
+ "long_press": "\" {subtype} \" on pikalt alla vajutatud",
+ "single_press": "\" {subtype} \" on vajutatud"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json
index 1e5671a67afa6d..9634fb784f761a 100644
--- a/homeassistant/components/homekit_controller/translations/fr.json
+++ b/homeassistant/components/homekit_controller/translations/fr.json
@@ -7,6 +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.",
"no_devices": "Aucun appareil non appair\u00e9 n'a pu \u00eatre trouv\u00e9"
},
"error": {
@@ -15,10 +16,11 @@
"max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.",
"max_tries_error": "Le p\u00e9riph\u00e9rique a refus\u00e9 d'ajouter le couplage car il a re\u00e7u plus de 100 tentatives d'authentification infructueuses.",
"pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.",
+ "protocol_error": "Erreur de communication avec l'accessoire. L'appareil peut ne pas \u00eatre en mode d'appairage et peut n\u00e9cessiter une pression sur un bouton physique ou virtuel.",
"unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.",
"unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9."
},
- "flow_title": "Accessoire HomeKit: {name}",
+ "flow_title": "{name} via le protocole accessoire HomeKit",
"step": {
"busy_error": {
"description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.",
@@ -32,21 +34,45 @@
"data": {
"pairing_code": "Code d\u2019appairage"
},
- "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.",
- "title": "Appairer avec l'accessoire HomeKit"
+ "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.",
+ "title": "Couplage avec un appareil via le protocole accessoire HomeKit"
},
"protocol_error": {
"description": "L\u2019appareil peut ne pas \u00eatre en mode appairement et peut n\u00e9cessiter une pression sur un bouton physique ou virtuel. Assurez-vous que l\u2019appareil est en mode appariement ou essayez de red\u00e9marrer l\u2019appareil, puis continuez \u00e0 reprendre l\u2019appariement.",
"title": "Erreur de communication avec l\u2019accessoire"
},
+ "try_pair_later": {
+ "description": "Assurez-vous que l'appareil est en mode de couplage ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 red\u00e9marrer le couplage.",
+ "title": "Couplage indisponible"
+ },
"user": {
"data": {
"device": "Appareil"
},
- "description": "S\u00e9lectionnez l'appareil avec lequel vous voulez appairer",
- "title": "Appairer avec l'accessoire HomeKit"
+ "description": "Le contr\u00f4leur HomeKit communique sur le r\u00e9seau local \u00e0 l'aide d'une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. S\u00e9lectionnez l'appareil avec lequel vous souhaitez vous associer:",
+ "title": "S\u00e9lection de l'appareil"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Bouton 1",
+ "button10": "Bouton 10",
+ "button2": "Bouton 2",
+ "button3": "Bouton 3",
+ "button4": "Bouton 4",
+ "button5": "Bouton 5",
+ "button6": "Bouton 6",
+ "button7": "Bouton 7",
+ "button8": "Bouton 8",
+ "button9": "Bouton 9",
+ "doorbell": "Sonnette"
+ },
+ "trigger_type": {
+ "double_press": "\" {subtype} \" appuy\u00e9 deux fois",
+ "long_press": "\" {subtype} \" enfonc\u00e9 et maintenu",
+ "single_press": "\" {subtype} \" press\u00e9"
+ }
+ },
"title": "Accessoire HomeKit"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json
index f07fd6df8b0008..e3f0a5c820eca5 100644
--- a/homeassistant/components/homekit_controller/translations/it.json
+++ b/homeassistant/components/homekit_controller/translations/it.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "Impossibile aggiungere l'abbinamento in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.",
"already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.",
- "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.",
"ignored_model": "Il supporto di HomeKit per questo modello \u00e8 bloccato poich\u00e9 \u00e8 disponibile un'integrazione nativa con pi\u00f9 funzionalit\u00e0.",
"invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che prima deve essere rimossa.",
+ "invalid_properties": "Propriet\u00e0 non valide annunciate dal dispositivo.",
"no_devices": "Non \u00e8 stato possibile trovare dispositivi non associati"
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "Impossibile abbinare, per favore riprova.",
"unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito."
},
- "flow_title": "Accessorio HomeKit: {name}",
+ "flow_title": "{name} tramite il Protocollo degli Accessori HomeKit",
"step": {
"busy_error": {
"description": "Interrompere l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continuare a riprendere l'associazione.",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "Codice di abbinamento"
},
- "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio",
- "title": "Abbina con accessorio HomeKit"
+ "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.",
+ "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori HomeKit"
},
"protocol_error": {
"description": "Il dispositivo potrebbe non essere in modalit\u00e0 di associazione e potrebbe richiedere una pressione di un pulsante fisico o virtuale. Assicurati che il dispositivo sia in modalit\u00e0 di associazione o prova a riavviarlo, quindi continua a riprendere l'associazione.",
@@ -48,10 +49,30 @@
"data": {
"device": "Dispositivo"
},
- "description": "Selezionare il dispositivo che si desidera abbinare",
- "title": "Abbina con accessorio HomeKit"
+ "description": "Il controller HomeKit comunica sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Seleziona il dispositivo che desideri associare:",
+ "title": "Selezione del dispositivo"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Pulsante 1",
+ "button10": "Pulsante 10",
+ "button2": "Pulsante 2",
+ "button3": "Pulsante 3",
+ "button4": "Pulsante 4",
+ "button5": "Pulsante 5",
+ "button6": "Pulsante 6",
+ "button7": "Pulsante 7",
+ "button8": "Pulsante 8",
+ "button9": "Pulsante 9",
+ "doorbell": "Campanello"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" premuto due volte",
+ "long_press": "\"{subtype}\" premuto e tenuto premuto",
+ "single_press": "\"{subtype}\" premuto"
+ }
+ },
"title": "Controller HomeKit"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json
index 55c5ee0053dc3f..a70a6269bb6bf4 100644
--- a/homeassistant/components/homekit_controller/translations/ko.json
+++ b/homeassistant/components/homekit_controller/translations/ko.json
@@ -7,6 +7,7 @@
"already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.",
+ "invalid_properties": "\uc7a5\uce58\uc5d0\uc11c\uc120\uc5b8\ud55c \uc798\ubabb\ub41c \uc18d\uc131\uc785\ub2c8\ub2e4.",
"no_devices": "\ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud55c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"error": {
@@ -48,5 +49,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\ubc84\ud2bc 1",
+ "button10": "\ubc84\ud2bc 10",
+ "button2": "\ubc84\ud2bc 2",
+ "button3": "\ubc84\ud2bc 3",
+ "button4": "\ubc84\ud2bc 4",
+ "button5": "\ubc84\ud2bc 5",
+ "button6": "\ubc84\ud2bc 6",
+ "button7": "\ubc84\ud2bc 7",
+ "button8": "\ubc84\ud2bc 8",
+ "button9": "\ubc84\ud2bc 9",
+ "doorbell": "\ucd08\uc778\uc885"
+ },
+ "trigger_type": {
+ "double_press": "\" {subtype} \"\uc744 \ub450\ubc88 \ub204\ub984",
+ "long_press": "\" {subtype} \"\uc744 \uae38\uac8c \ub204\ub984",
+ "single_press": "\"{subtype}\" \uc744 \ud55c\ubc88 \ub204\ub984"
+ }
+ },
"title": "HomeKit \ucee8\ud2b8\ub864\ub7ec"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/lb.json b/homeassistant/components/homekit_controller/translations/lb.json
index 5431b3b30d1628..7625d0c561ea41 100644
--- a/homeassistant/components/homekit_controller/translations/lb.json
+++ b/homeassistant/components/homekit_controller/translations/lb.json
@@ -7,6 +7,7 @@
"already_paired": "D\u00ebsen Accessoire ass schonn mat engem aneren Apparat verbonnen. S\u00ebtzt den Apparat op Wierksastellungen zer\u00e9ck an prob\u00e9iert nach emol w.e.g.",
"ignored_model": "HomeKit Support fir d\u00ebse Modell ass block\u00e9iert well eng m\u00e9i komplett nativ Integratioun disponibel ass.",
"invalid_config_entry": "D\u00ebsen Apparat mellt sech prett fir ze verbanne mee et g\u00ebtt schonn eng Entr\u00e9e am Home Assistant d\u00e9i ee Konflikt duerstellt welch fir d'\u00e9ischt muss erausgeholl ginn.",
+ "invalid_properties": "Ong\u00eblteg Eegeschafte vum Apparat annonc\u00e9iert",
"no_devices": "Keng net verbonnen Apparater fonnt"
},
"error": {
@@ -53,5 +54,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Kn\u00e4ppchen 1",
+ "button10": "Kn\u00e4ppchen 10",
+ "button2": "Kn\u00e4ppchen 2",
+ "button3": "Kn\u00e4ppchen 3",
+ "button4": "Kn\u00e4ppchen 4",
+ "button5": "Kn\u00e4ppchen 5",
+ "button6": "Kn\u00e4ppchen 6",
+ "button7": "Kn\u00e4ppchen 7",
+ "button8": "Kn\u00e4ppchen 8",
+ "button9": "Kn\u00e4ppchen 9",
+ "doorbell": "Schell"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" zwee mol gedr\u00e9ckt",
+ "long_press": "\"{subtype}\" gedr\u00e9ckt an ugehal",
+ "single_press": "\"{subtype}\" gedr\u00e9ckt"
+ }
+ },
"title": "HomeKit Kontroller"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json
index 20013168c81503..84f5495cca7f7f 100644
--- a/homeassistant/components/homekit_controller/translations/nl.json
+++ b/homeassistant/components/homekit_controller/translations/nl.json
@@ -7,6 +7,7 @@
"already_paired": "Dit accessoire is al gekoppeld aan een ander apparaat. Reset het accessoire en probeer het opnieuw.",
"ignored_model": "HomeKit-ondersteuning voor dit model is geblokkeerd omdat er een meer functie volledige native integratie beschikbaar is.",
"invalid_config_entry": "Dit apparaat geeft aan dat het gereed is om te koppelen, maar er is al een conflicterend configuratie-item voor in de Home Assistant dat eerst moet worden verwijderd.",
+ "invalid_properties": "Ongeldige eigenschappen aangekondigd door apparaat.",
"no_devices": "Er zijn geen gekoppelde apparaten gevonden"
},
"error": {
@@ -36,5 +37,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Knop 1",
+ "button10": "Knop 10",
+ "button2": "Knop 2",
+ "button3": "Knop 3",
+ "button4": "Knop 4",
+ "button5": "Knop 5",
+ "button6": "Knop 6",
+ "button7": "Knop 7",
+ "button8": "Knop 8",
+ "button9": "Knop 9",
+ "doorbell": "Deurbel"
+ },
+ "trigger_type": {
+ "double_press": "\" {subtype} \" tweemaal ingedrukt",
+ "long_press": "\"{subtype}\" ingedrukt en vastgehouden",
+ "single_press": "\" {subtype} \" ingedrukt"
+ }
+ },
"title": "HomeKit Accessoires"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json
index 45444d3d9d053d..5b37ba68e24c11 100644
--- a/homeassistant/components/homekit_controller/translations/no.json
+++ b/homeassistant/components/homekit_controller/translations/no.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "Kan ikke legge til sammenkobling da enheten ikke lenger kan bli funnet.",
"already_configured": "Tilbeh\u00f8r er allerede konfigurert med denne kontrolleren.",
- "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.",
"ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrasjon er tilgjengelig.",
"invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.",
+ "invalid_properties": "Ugyldige egenskaper kunngjort av enheten.",
"no_devices": "Ingen ukoblede enheter ble funnet"
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.",
"unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes."
},
- "flow_title": "HomeKit Tilbeh\u00f8r: {name}",
+ "flow_title": "{name} via HomeKit Accessory Protocol",
"step": {
"busy_error": {
"description": "Avbryt sammenkobling p\u00e5 alle kontrollere, eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter med \u00e5 fortsette sammenkoblingen.",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "Sammenkoblingskode"
},
- "description": "Angi din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret",
- "title": "Koble til HomeKit tilbeh\u00f8r"
+ "description": "HomeKit Controller kommuniserer med {name} over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.",
+ "title": "Par med en enhet via HomeKit Accessory Protocol"
},
"protocol_error": {
"description": "Enheten er kanskje ikke i paringsmodus og kan kreve et fysisk eller virtuelt knappetrykk. Kontroller at enheten er i paringsmodus eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter \u00e5 fortsette paringen.",
@@ -48,10 +49,30 @@
"data": {
"device": "Enhet"
},
- "description": "Velg enheten du vil koble til",
- "title": "Koble til HomeKit tilbeh\u00f8r"
+ "description": "HomeKit Controller kommuniserer over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Velg enheten du vil pare med:",
+ "title": "Valg av enhet"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Knapp 1",
+ "button10": "Knapp 10",
+ "button2": "Knapp 2",
+ "button3": "Knapp 3",
+ "button4": "Knapp 4",
+ "button5": "Knapp 5",
+ "button6": "Knapp 6",
+ "button7": "Knapp 7",
+ "button8": "Knapp 8",
+ "button9": "Knapp 9",
+ "doorbell": "D\u00f8r-klokke"
+ },
+ "trigger_type": {
+ "double_press": "{subtype} trykket to ganger",
+ "long_press": "{subtype} trykket og holdt",
+ "single_press": "{subtype}\u00bb trykket"
+ }
+ },
"title": "HomeKit-kontroller"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json
index bed897ea2424d7..a819cea588145b 100644
--- a/homeassistant/components/homekit_controller/translations/pl.json
+++ b/homeassistant/components/homekit_controller/translations/pl.json
@@ -7,6 +7,7 @@
"already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.",
"ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.",
"invalid_config_entry": "To urz\u0105dzenie jest wy\u015bwietlane jako gotowe do sparowania, ale istnieje ju\u017c konfliktowy wpis konfiguracyjny dla niego w Home Assistant, kt\u00f3ry musi zosta\u0107 najpierw usuni\u0119ty.",
+ "invalid_properties": "Urz\u0105dzenie zg\u0142osi\u0142o nieprawid\u0142owe w\u0142a\u015bciwo\u015bci.",
"no_devices": "Nie znaleziono niesparowanych urz\u0105dze\u0144."
},
"error": {
@@ -15,11 +16,20 @@
"max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania.",
"max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o dodania parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.",
"pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.",
+ "protocol_error": "B\u0142\u0105d w komunikacji z urz\u0105dzeniem. Urz\u0105dzenie mo\u017ce nie by\u0107 w trybie parowania i mo\u017ce wymaga\u0107 fizycznego lub wirtualnego naci\u015bni\u0119cia przycisku.",
"unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie.",
"unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119."
},
"flow_title": "Akcesoria HomeKit: {name}",
"step": {
+ "busy_error": {
+ "description": "Przerwij parowanie we wszystkich kontrolerach lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie.",
+ "title": "Urz\u0105dzenie jest ju\u017c sparowane z innym kontrolerem"
+ },
+ "max_tries_error": {
+ "description": "Urz\u0105dzenie otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia. Spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie.",
+ "title": "Przekroczono maksymaln\u0105 liczb\u0119 pr\u00f3b uwierzytelnienia"
+ },
"pair": {
"data": {
"pairing_code": "Kod parowania"
@@ -28,7 +38,12 @@
"title": "Sparuj z akcesorium HomeKit"
},
"protocol_error": {
- "description": "Urz\u0105dzenie mo\u017ce nie by\u0107 w trybie parowania i wymaga\u0107 "
+ "description": "Urz\u0105dzenie mo\u017ce nie by\u0107 w trybie parowania i mo\u017ce wymaga\u0107 fizycznego lub wirtualnego naci\u015bni\u0119cia przycisku. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w trybie parowania lub spr\u00f3buj je ponownie uruchomi\u0107, a nast\u0119pnie wzn\u00f3w parowanie.",
+ "title": "B\u0142\u0105d komunikacji z akcesorium"
+ },
+ "try_pair_later": {
+ "description": "Upewnij si\u0119, \u017ce urz\u0105dzenie jest w trybie parowania lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie spr\u00f3buj ponownego parowania.",
+ "title": "Parowanie niedost\u0119pne"
},
"user": {
"data": {
@@ -39,5 +54,25 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Przycisk 1",
+ "button10": "Przycisk 10",
+ "button2": "Przycisk 2",
+ "button3": "Przycisk 3",
+ "button4": "Przycisk 4",
+ "button5": "Przycisk 5",
+ "button6": "Przycisk 6",
+ "button7": "Przycisk 7",
+ "button8": "Przycisk 8",
+ "button9": "Przycisk 9",
+ "doorbell": "Dzwonek do drzwi"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" naci\u015bni\u0119ty dwukrotnie",
+ "long_press": "\"{subtype}\" naci\u015bni\u0119ty i przytrzymany",
+ "single_press": "\"{subtype}\" naci\u015bni\u0119ty"
+ }
+ },
"title": "Akcesorium HomeKit"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json
index fb9612ef98130b..791dfc36cb43b6 100644
--- a/homeassistant/components/homekit_controller/translations/ru.json
+++ b/homeassistant/components/homekit_controller/translations/ru.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
"already_configured": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u044d\u0442\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
"ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.",
"invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.",
+ "invalid_properties": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430, \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c.",
"no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0434\u043b\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b."
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
"unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c."
},
- "flow_title": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 HomeKit: {name}",
+ "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit",
"step": {
"busy_error": {
"description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.",
- "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit"
+ "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.",
+ "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit"
},
"protocol_error": {
"description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u043d\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0438 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0438\u043b\u0438 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0438. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.",
@@ -48,10 +49,30 @@
"data": {
"device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
},
- "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.",
- "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit"
+ "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0433\u043e \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435:",
+ "title": "\u0412\u044b\u0431\u043e\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\u041a\u043d\u043e\u043f\u043a\u0430 1",
+ "button10": "\u041a\u043d\u043e\u043f\u043a\u0430 10",
+ "button2": "\u041a\u043d\u043e\u043f\u043a\u0430 2",
+ "button3": "\u041a\u043d\u043e\u043f\u043a\u0430 3",
+ "button4": "\u041a\u043d\u043e\u043f\u043a\u0430 4",
+ "button5": "\u041a\u043d\u043e\u043f\u043a\u0430 5",
+ "button6": "\u041a\u043d\u043e\u043f\u043a\u0430 6",
+ "button7": "\u041a\u043d\u043e\u043f\u043a\u0430 7",
+ "button8": "\u041a\u043d\u043e\u043f\u043a\u0430 8",
+ "button9": "\u041a\u043d\u043e\u043f\u043a\u0430 9",
+ "doorbell": "\u0414\u0432\u0435\u0440\u043d\u043e\u0439 \u0437\u0432\u043e\u043d\u043e\u043a"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b",
+ "long_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f",
+ "single_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430"
+ }
+ },
"title": "HomeKit Controller"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json
index 0c57e09b09b709..e57d61dcdb6e2b 100644
--- a/homeassistant/components/homekit_controller/translations/sv.json
+++ b/homeassistant/components/homekit_controller/translations/sv.json
@@ -36,5 +36,20 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Knapp 1",
+ "button10": "Knapp 10",
+ "button2": "Knapp 2",
+ "button3": "Knapp 3",
+ "button4": "Knapp 4",
+ "button5": "Knapp 5",
+ "button6": "Knapp 6",
+ "button7": "Knapp 7",
+ "button8": "Knapp 8",
+ "button9": "Knapp 9",
+ "doorbell": "D\u00f6rrklocka"
+ }
+ },
"title": "HomeKit-tillbeh\u00f6r"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json
index 51f521d5c358f7..ded672bd83846e 100644
--- a/homeassistant/components/homekit_controller/translations/zh-Hant.json
+++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json
@@ -3,10 +3,11 @@
"abort": {
"accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002",
"already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002",
"ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002",
"invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u5be6\u9ad4\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002",
+ "invalid_properties": "\u8a2d\u5099\u5ba3\u544a\u5c6c\u6027\u7121\u6548\u3002",
"no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099"
},
"error": {
@@ -19,7 +20,7 @@
"unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
"unknown_error": "\u8a2d\u5099\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002"
},
- "flow_title": "HomeKit \u914d\u4ef6\uff1a{name}",
+ "flow_title": "{name} \u4f7f\u7528 HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a",
"step": {
"busy_error": {
"description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u8a2d\u5099\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002",
@@ -33,8 +34,8 @@
"data": {
"pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc"
},
- "description": "\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6",
- "title": "HomeKit \u914d\u4ef6\u914d\u5c0d"
+ "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u8a2d\u5099\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002",
+ "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u8a2d\u5099"
},
"protocol_error": {
"description": "\u8a2d\u5099\u4e26\u672a\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff0c\u53ef\u80fd\u9700\u8981\u6309\u4e0b\u5be6\u9ad4\u6216\u865b\u64ec\u6309\u9215\u3002\u8acb\u78ba\u5b9a\u8a2d\u5099\u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u6216\u91cd\u555f\u8a2d\u5099\uff0c\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002",
@@ -48,10 +49,30 @@
"data": {
"device": "\u8a2d\u5099"
},
- "description": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684\u8a2d\u5099",
- "title": "HomeKit \u914d\u4ef6\u914d\u5c0d"
+ "description": "\u4f7f\u7528\u5340\u57df\u7db2\u8def\u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u9078\u64c7\u6240\u8981\u65b0\u589e\u914d\u5c0d\u7684\u8a2d\u5099\uff1a",
+ "title": "\u8a2d\u5099\u9078\u64c7"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\u6309\u9215 1",
+ "button10": "\u6309\u9215 10",
+ "button2": "\u6309\u9215 2",
+ "button3": "\u6309\u9215 3",
+ "button4": "\u6309\u9215 4",
+ "button5": "\u6309\u9215 5",
+ "button6": "\u6309\u9215 6",
+ "button7": "\u6309\u9215 7",
+ "button8": "\u6309\u9215 8",
+ "button9": "\u6309\u9215 9",
+ "doorbell": "\u9580\u9234"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" \u6309\u4e0b\u5169\u6b21",
+ "long_press": "\"{subtype}\" \u6309\u4e0b\u4e26\u6309\u4f4f",
+ "single_press": "\"{subtype}\" \u6309\u4e0b"
+ }
+ },
"title": "HomeKit \u63a7\u5236\u5668"
}
\ No newline at end of file
diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py
index 1ce18a8e7593b0..22ac7972c4da01 100644
--- a/homeassistant/components/homematic/const.py
+++ b/homeassistant/components/homematic/const.py
@@ -57,6 +57,8 @@
"IPKeySwitchLevel",
"IPMultiIO",
"IPWSwitch",
+ "IOSwitchWireless",
+ "IPWIODevice",
],
DISCOVER_LIGHTS: [
"Dimmer",
@@ -111,7 +113,7 @@
"IPThermostatWall2",
"IPRemoteMotionV2",
"HBUNISenWEA",
- "IPWMotionDection",
+ "PresenceIPW",
],
DISCOVER_CLIMATE: [
"Thermostat",
@@ -154,6 +156,7 @@
"IPRemoteMotionV2",
"IPWInputDevice",
"IPWMotionDection",
+ "IPAlarmSensor",
],
DISCOVER_COVER: [
"Blind",
diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py
index 49d3ee1f1704d3..8578be93555194 100644
--- a/homeassistant/components/homematic/entity.py
+++ b/homeassistant/components/homematic/entity.py
@@ -233,8 +233,7 @@ def state(self):
@property
def state_attributes(self):
"""Return the state attributes."""
- attr = self._variables.copy()
- return attr
+ return self._variables.copy()
@property
def icon(self):
diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json
index d8c60c0f9765f4..63e33a60c536ea 100644
--- a/homeassistant/components/homematic/manifest.json
+++ b/homeassistant/components/homematic/manifest.json
@@ -2,6 +2,6 @@
"domain": "homematic",
"name": "Homematic",
"documentation": "https://www.home-assistant.io/integrations/homematic",
- "requirements": ["pyhomematic==0.1.68"],
+ "requirements": ["pyhomematic==0.1.70"],
"codeowners": ["@pvizeli", "@danielperna84"]
}
diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py
index 51a88bd120778e..e6439c451c1670 100644
--- a/homeassistant/components/homematic/sensor.py
+++ b/homeassistant/components/homematic/sensor.py
@@ -9,8 +9,11 @@
DEVICE_CLASS_TEMPERATURE,
ENERGY_WATT_HOUR,
FREQUENCY_HERTZ,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
VOLT,
@@ -48,18 +51,18 @@
"ENERGY_COUNTER": ENERGY_WATT_HOUR,
"GAS_POWER": VOLUME_CUBIC_METERS,
"GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS,
- "LUX": "lx",
- "ILLUMINATION": "lx",
- "CURRENT_ILLUMINATION": "lx",
- "AVERAGE_ILLUMINATION": "lx",
- "LOWEST_ILLUMINATION": "lx",
- "HIGHEST_ILLUMINATION": "lx",
- "RAIN_COUNTER": "mm",
+ "LUX": LIGHT_LUX,
+ "ILLUMINATION": LIGHT_LUX,
+ "CURRENT_ILLUMINATION": LIGHT_LUX,
+ "AVERAGE_ILLUMINATION": LIGHT_LUX,
+ "LOWEST_ILLUMINATION": LIGHT_LUX,
+ "HIGHEST_ILLUMINATION": LIGHT_LUX,
+ "RAIN_COUNTER": LENGTH_MILLIMETERS,
"WIND_SPEED": SPEED_KILOMETERS_PER_HOUR,
"WIND_DIRECTION": DEGREE,
"WIND_DIRECTION_RANGE": DEGREE,
"SUNSHINEDURATION": "#",
- "AIR_PRESSURE": "hPa",
+ "AIR_PRESSURE": PRESSURE_HPA,
"FREQUENCY": FREQUENCY_HERTZ,
"VALUE": "#",
"VALVE_STATE": PERCENTAGE,
diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py
index 47da33e86dab8c..e53307c533bf9d 100644
--- a/homeassistant/components/homematicip_cloud/__init__.py
+++ b/homeassistant/components/homematicip_cloud/__init__.py
@@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
# Add the HAP name from configuration if set.
hapname = home.label if not home.name else f"{home.name} {home.label}"
device_registry.async_get_or_create(
- config_entry_id=home.id,
+ config_entry_id=entry.entry_id,
identifiers={(DOMAIN, home.id)},
manufacturer="eQ-3",
name=hapname,
diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index 50e8360675b2fc..3c04fd7410a7c4 100644
--- a/homeassistant/components/homematicip_cloud/binary_sensor.py
+++ b/homeassistant/components/homematicip_cloud/binary_sensor.py
@@ -82,7 +82,7 @@ async def async_setup_entry(
) -> None:
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id]
- entities = []
+ entities = [HomematicipCloudConnectionSensor(hap)]
for device in hap.home.devices:
if isinstance(device, AsyncAccelerationSensor):
entities.append(HomematicipAccelerationSensor(hap, device))
@@ -128,14 +128,52 @@ async def async_setup_entry(
for group in hap.home.groups:
if isinstance(group, AsyncSecurityGroup):
- entities.append(HomematicipSecuritySensorGroup(hap, group))
+ entities.append(HomematicipSecuritySensorGroup(hap, device=group))
elif isinstance(group, AsyncSecurityZoneGroup):
- entities.append(HomematicipSecurityZoneSensorGroup(hap, group))
+ entities.append(HomematicipSecurityZoneSensorGroup(hap, device=group))
if entities:
async_add_entities(entities)
+class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEntity):
+ """Representation of the HomematicIP cloud connection sensor."""
+
+ def __init__(self, hap: HomematicipHAP) -> None:
+ """Initialize the cloud connection sensor."""
+ super().__init__(hap, hap.home, "Cloud Connection")
+
+ @property
+ def device_info(self) -> Dict[str, Any]:
+ """Return device specific attributes."""
+ # Adds a sensor to the existing HAP device
+ return {
+ "identifiers": {
+ # Serial numbers of Homematic IP device
+ (HMIPC_DOMAIN, self._home.id)
+ }
+ }
+
+ @property
+ def icon(self) -> str:
+ """Return the icon of the access point entity."""
+ return (
+ "mdi:access-point-network"
+ if self._home.connected
+ else "mdi:access-point-network-off"
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if hap is connected to cloud."""
+ return self._home.connected
+
+ @property
+ def available(self) -> bool:
+ """Sensor is always available."""
+ return True
+
+
class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity):
"""Representation of the HomematicIP base action sensor."""
@@ -323,7 +361,7 @@ class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize sunshine sensor."""
- super().__init__(hap, device, "Sunshine")
+ super().__init__(hap, device, post="Sunshine")
@property
def device_class(self) -> str:
@@ -352,7 +390,7 @@ class HomematicipBatterySensor(HomematicipGenericEntity, BinarySensorEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize battery sensor."""
- super().__init__(hap, device, "Battery")
+ super().__init__(hap, device, post="Battery")
@property
def device_class(self) -> str:
@@ -391,7 +429,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE
def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None:
"""Initialize security zone group."""
device.modelType = f"HmIP-{post}"
- super().__init__(hap, device, post)
+ super().__init__(hap, device, post=post)
@property
def device_class(self) -> str:
@@ -447,7 +485,7 @@ class HomematicipSecuritySensorGroup(
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize security group."""
- super().__init__(hap, device, "Sensors")
+ super().__init__(hap, device, post="Sensors")
@property
def device_state_attributes(self) -> Dict[str, Any]:
diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py
index 3a19c1b2afe921..ce8b44f5702c09 100644
--- a/homeassistant/components/homematicip_cloud/generic_entity.py
+++ b/homeassistant/components/homematicip_cloud/generic_entity.py
@@ -70,12 +70,19 @@
class HomematicipGenericEntity(Entity):
"""Representation of the HomematicIP generic entity."""
- def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None:
+ def __init__(
+ self,
+ hap: HomematicipHAP,
+ device,
+ post: Optional[str] = None,
+ channel: Optional[int] = None,
+ ) -> None:
"""Initialize the generic entity."""
self._hap = hap
self._home = hap.home
self._device = device
- self.post = post
+ self._post = post
+ self._channel = channel
# Marker showing that the HmIP device hase been removed.
self.hmip_device_removed = False
_LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType)
@@ -94,6 +101,7 @@ def device_info(self) -> Dict[str, Any]:
"manufacturer": self._device.oem,
"model": self._device.modelType,
"sw_version": self._device.firmwareVersion,
+ # Link to the homematic ip access point.
"via_device": (HMIPC_DOMAIN, self._device.homeId),
}
return None
@@ -167,18 +175,28 @@ def _async_device_removed(self, *args, **kwargs) -> None:
@property
def name(self) -> str:
"""Return the name of the generic entity."""
- name = self._device.label
- if name and self._home.name:
- name = f"{self._home.name} {name}"
- if name and self.post:
- name = f"{name} {self.post}"
- return name
- def _get_label_by_channel(self, channel: int) -> str:
- """Return the name of the channel."""
- name = self._device.functionalChannels[channel].label
+ name = None
+ # Try to get a label from a channel.
+ if hasattr(self._device, "functionalChannels"):
+ if self._channel:
+ name = self._device.functionalChannels[self._channel].label
+ else:
+ if len(self._device.functionalChannels) > 1:
+ name = self._device.functionalChannels[1].label
+
+ # Use device label, if name is not defined by channel label.
+ if not name:
+ name = self._device.label
+ if self._post:
+ name = f"{name} {self._post}"
+ elif self._channel:
+ name = f"{name} Channel{self._channel}"
+
+ # Add a prefix to the name if the homematic ip home has a name.
if name and self._home.name:
name = f"{self._home.name} {name}"
+
return name
@property
@@ -194,7 +212,13 @@ def available(self) -> bool:
@property
def unique_id(self) -> str:
"""Return a unique ID."""
- return f"{self.__class__.__name__}_{self._device.id}"
+ unique_id = f"{self.__class__.__name__}_{self._device.id}"
+ if self._channel:
+ unique_id = (
+ f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
+ )
+
+ return unique_id
@property
def icon(self) -> Optional[str]:
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index 727371223727b9..e4d85c00b19667 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -71,14 +71,6 @@ def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the light entity."""
super().__init__(hap, device)
- @property
- def name(self) -> str:
- """Return the name of the multi switch channel."""
- label = self._get_label_by_channel(1)
- if label:
- return label
- return super().name
-
@property
def is_on(self) -> bool:
"""Return true if light is on."""
@@ -149,11 +141,10 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
def __init__(self, hap: HomematicipHAP, device, channel: int) -> None:
"""Initialize the notification light entity."""
- self.channel = channel
- if self.channel == 2:
- super().__init__(hap, device, "Top")
+ if channel == 2:
+ super().__init__(hap, device, post="Top", channel=channel)
else:
- super().__init__(hap, device, "Bottom")
+ super().__init__(hap, device, post="Bottom", channel=channel)
self._color_switcher = {
RGBColorState.WHITE: [0.0, 0.0],
@@ -167,7 +158,7 @@ def __init__(self, hap: HomematicipHAP, device, channel: int) -> None:
@property
def _func_channel(self) -> NotificationLightChannel:
- return self._device.functionalChannels[self.channel]
+ return self._device.functionalChannels[self._channel]
@property
def is_on(self) -> bool:
@@ -198,14 +189,6 @@ def device_state_attributes(self) -> Dict[str, Any]:
return state_attr
- @property
- def name(self) -> str:
- """Return the name of the notification light sensor."""
- label = self._get_label_by_channel(self.channel)
- if label:
- return label
- return f"{super().name} Notification"
-
@property
def supported_features(self) -> int:
"""Flag supported features."""
@@ -214,7 +197,7 @@ def supported_features(self) -> int:
@property
def unique_id(self) -> str:
"""Return a unique ID."""
- return f"{self.__class__.__name__}_{self.post}_{self._device.id}"
+ return f"{self.__class__.__name__}_{self._post}_{self._device.id}"
async def async_turn_on(self, **kwargs) -> None:
"""Turn the light on."""
@@ -237,7 +220,7 @@ async def async_turn_on(self, **kwargs) -> None:
transition = kwargs.get(ATTR_TRANSITION, 0.5)
await self._device.set_rgb_dim_level_with_time(
- channelIndex=self.channel,
+ channelIndex=self._channel,
rgb=simple_rgb_color,
dimLevel=dim_level,
onTime=0,
@@ -250,7 +233,7 @@ async def async_turn_off(self, **kwargs) -> None:
transition = kwargs.get(ATTR_TRANSITION, 0.5)
await self._device.set_rgb_dim_level_with_time(
- channelIndex=self.channel,
+ channelIndex=self._channel,
rgb=simple_rgb_color,
dimLevel=0.0,
onTime=0,
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index 32191cde20e486..77d7560e622f98 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -30,6 +30,8 @@
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
SPEED_KILOMETERS_PER_HOUR,
@@ -125,7 +127,7 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize access point status entity."""
- super().__init__(hap, hap.home)
+ super().__init__(hap, device=hap.home, post="Duty Cycle")
@property
def device_info(self) -> Dict[str, Any]:
@@ -134,7 +136,7 @@ def device_info(self) -> Dict[str, Any]:
return {
"identifiers": {
# Serial numbers of Homematic IP device
- (HMIPC_DOMAIN, self._device.id)
+ (HMIPC_DOMAIN, self._home.id)
}
}
@@ -174,7 +176,7 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize heating thermostat device."""
- super().__init__(hap, device, "Heating")
+ super().__init__(hap, device, post="Heating")
@property
def icon(self) -> str:
@@ -203,7 +205,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the thermometer device."""
- super().__init__(hap, device, "Humidity")
+ super().__init__(hap, device, post="Humidity")
@property
def device_class(self) -> str:
@@ -226,7 +228,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the thermometer device."""
- super().__init__(hap, device, "Temperature")
+ super().__init__(hap, device, post="Temperature")
@property
def device_class(self) -> str:
@@ -263,7 +265,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(hap, device, "Illuminance")
+ super().__init__(hap, device, post="Illuminance")
@property
def device_class(self) -> str:
@@ -281,7 +283,7 @@ def state(self) -> float:
@property
def unit_of_measurement(self) -> str:
"""Return the unit this state is expressed in."""
- return "lx"
+ return LIGHT_LUX
@property
def device_state_attributes(self) -> Dict[str, Any]:
@@ -301,7 +303,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(hap, device, "Power")
+ super().__init__(hap, device, post="Power")
@property
def device_class(self) -> str:
@@ -324,7 +326,7 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the windspeed sensor."""
- super().__init__(hap, device, "Windspeed")
+ super().__init__(hap, device, post="Windspeed")
@property
def state(self) -> float:
@@ -357,7 +359,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity):
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
- super().__init__(hap, device, "Today Rain")
+ super().__init__(hap, device, post="Today Rain")
@property
def state(self) -> float:
@@ -367,7 +369,7 @@ def state(self) -> float:
@property
def unit_of_measurement(self) -> str:
"""Return the unit this state is expressed in."""
- return "mm"
+ return LENGTH_MILLIMETERS
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity):
diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json
index fddb2b85df69dc..1efafdc6e29337 100644
--- a/homeassistant/components/homematicip_cloud/strings.json
+++ b/homeassistant/components/homematicip_cloud/strings.json
@@ -5,8 +5,8 @@
"title": "Pick HomematicIP Access point",
"data": {
"hapid": "Access point ID (SGTIN)",
- "pin": "Pin Code (optional)",
- "name": "Name (optional, used as name prefix for all devices)"
+ "pin": "[%key:common::config_flow::data::pin%]",
+ "name": "[%key:common::config_flow::data::name%] (optional, used as name prefix for all devices)"
}
},
"link": {
@@ -21,9 +21,9 @@
"timeout_button": "Blue button press timeout, please try again."
},
"abort": {
- "unknown": "Unknown error occurred.",
- "connection_aborted": "Could not connect to HMIP server",
- "already_configured": "Access point is already configured"
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "connection_aborted": "[%key:common::config_flow::error::cannot_connect%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py
index 64ee862b2d2345..b9fbdad651a26a 100644
--- a/homeassistant/components/homematicip_cloud/switch.py
+++ b/homeassistant/components/homematicip_cloud/switch.py
@@ -54,16 +54,16 @@ async def async_setup_entry(
entities.append(HomematicipSwitch(hap, device))
elif isinstance(device, AsyncOpenCollector8Module):
for channel in range(1, 9):
- entities.append(HomematicipMultiSwitch(hap, device, channel))
+ entities.append(HomematicipMultiSwitch(hap, device, channel=channel))
elif isinstance(device, AsyncHeatingSwitch2):
for channel in range(1, 3):
- entities.append(HomematicipMultiSwitch(hap, device, channel))
+ entities.append(HomematicipMultiSwitch(hap, device, channel=channel))
elif isinstance(device, AsyncMultiIOBox):
for channel in range(1, 3):
- entities.append(HomematicipMultiSwitch(hap, device, channel))
+ entities.append(HomematicipMultiSwitch(hap, device, channel=channel))
elif isinstance(device, AsyncPrintedCircuitBoardSwitch2):
for channel in range(1, 3):
- entities.append(HomematicipMultiSwitch(hap, device, channel))
+ entities.append(HomematicipMultiSwitch(hap, device, channel=channel))
for group in hap.home.groups:
if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)):
@@ -156,31 +156,17 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity):
def __init__(self, hap: HomematicipHAP, device, channel: int) -> None:
"""Initialize the multi switch device."""
- self.channel = channel
- super().__init__(hap, device, f"Channel{channel}")
-
- @property
- def name(self) -> str:
- """Return the name of the multi switch channel."""
- label = self._get_label_by_channel(self.channel)
- if label:
- return label
- return super().name
-
- @property
- def unique_id(self) -> str:
- """Return a unique ID."""
- return f"{self.__class__.__name__}_{self.post}_{self._device.id}"
+ super().__init__(hap, device, channel=channel)
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
- return self._device.functionalChannels[self.channel].on
+ return self._device.functionalChannels[self._channel].on
async def async_turn_on(self, **kwargs) -> None:
"""Turn the switch on."""
- await self._device.turn_on(self.channel)
+ await self._device.turn_on(self._channel)
async def async_turn_off(self, **kwargs) -> None:
"""Turn the switch off."""
- await self._device.turn_off(self.channel)
+ await self._device.turn_off(self._channel)
diff --git a/homeassistant/components/homematicip_cloud/translations/ca.json b/homeassistant/components/homematicip_cloud/translations/ca.json
index 871af68e360b57..26892977185841 100644
--- a/homeassistant/components/homematicip_cloud/translations/ca.json
+++ b/homeassistant/components/homematicip_cloud/translations/ca.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "El punt d'acc\u00e9s ja est\u00e0 configurat",
- "connection_aborted": "No s'ha pogut connectar al servidor HMIP",
- "unknown": "S'ha produ\u00eft un error desconegut."
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "connection_aborted": "Ha fallat la connexi\u00f3",
+ "unknown": "Error inesperat"
},
"error": {
"invalid_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.",
@@ -16,7 +16,7 @@
"init": {
"data": {
"hapid": "Identificador del punt d'acc\u00e9s (SGTIN)",
- "name": "Nom (opcional, s'utilitza com a nom prefix per a tots els dispositius)",
+ "name": "Nom (opcional, s'utilitza com a prefix de nom per a tots els dispositius)",
"pin": "Codi PIN (opcional)"
},
"title": "Tria el punt d'acc\u00e9s HomematicIP"
diff --git a/homeassistant/components/homematicip_cloud/translations/el.json b/homeassistant/components/homematicip_cloud/translations/el.json
new file mode 100644
index 00000000000000..843d590e7e0ae1
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf PIN, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/translations/en.json b/homeassistant/components/homematicip_cloud/translations/en.json
index 97cfd1e3f4e46c..acddc0766c4d37 100644
--- a/homeassistant/components/homematicip_cloud/translations/en.json
+++ b/homeassistant/components/homematicip_cloud/translations/en.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Access point is already configured",
- "connection_aborted": "Could not connect to HMIP server",
- "unknown": "Unknown error occurred."
+ "already_configured": "Device is already configured",
+ "connection_aborted": "Failed to connect",
+ "unknown": "Unexpected error"
},
"error": {
"invalid_pin": "Invalid PIN, please try again.",
diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json
index b5877fe09ce270..cd300d4e4b3bd7 100644
--- a/homeassistant/components/homematicip_cloud/translations/es.json
+++ b/homeassistant/components/homematicip_cloud/translations/es.json
@@ -6,6 +6,7 @@
"unknown": "Se ha producido un error desconocido."
},
"error": {
+ "invalid_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.",
"invalid_sgtin_or_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.",
"press_the_button": "Por favor, pulsa el bot\u00f3n azul",
"register_failed": "No se pudo registrar, por favor intentelo de nuevo.",
diff --git a/homeassistant/components/homematicip_cloud/translations/et.json b/homeassistant/components/homematicip_cloud/translations/et.json
index 92f07d401e633b..4f97508c045661 100644
--- a/homeassistant/components/homematicip_cloud/translations/et.json
+++ b/homeassistant/components/homematicip_cloud/translations/et.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "invalid_pin": "Vale PIN-kood. Palun proovige uuesti.",
"invalid_sgtin_or_pin": "Vale PIN, palun proovige uuesti"
},
"step": {
diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json
index 585334b31183ff..0c5f54d588ab04 100644
--- a/homeassistant/components/homematicip_cloud/translations/fr.json
+++ b/homeassistant/components/homematicip_cloud/translations/fr.json
@@ -7,7 +7,7 @@
},
"error": {
"invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.",
- "invalid_sgtin_or_pin": "Code PIN invalide, veuillez r\u00e9essayer.",
+ "invalid_sgtin_or_pin": "Code SGTIN ou PIN invalide, veuillez r\u00e9essayer.",
"press_the_button": "Veuillez appuyer sur le bouton bleu.",
"register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.",
"timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer."
diff --git a/homeassistant/components/homematicip_cloud/translations/it.json b/homeassistant/components/homematicip_cloud/translations/it.json
index 4e7bfd1108ca99..fc6dc5aa55ad0c 100644
--- a/homeassistant/components/homematicip_cloud/translations/it.json
+++ b/homeassistant/components/homematicip_cloud/translations/it.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Il punto di accesso \u00e8 gi\u00e0 configurato",
- "connection_aborted": "Impossibile connettersi al server HMIP",
- "unknown": "Si \u00e8 verificato un errore sconosciuto."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "connection_aborted": "Impossibile connettersi",
+ "unknown": "Errore imprevisto"
},
"error": {
"invalid_pin": "PIN non valido, riprova.",
diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json
index b85b8ac00b1153..962faa680664a9 100644
--- a/homeassistant/components/homematicip_cloud/translations/ko.json
+++ b/homeassistant/components/homematicip_cloud/translations/ko.json
@@ -6,6 +6,7 @@
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "invalid_pin": "\uc798\ubabb\ub41c PIN\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uc2ed\uc2dc\uc624.",
"invalid_sgtin_or_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.",
"register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
diff --git a/homeassistant/components/homematicip_cloud/translations/lb.json b/homeassistant/components/homematicip_cloud/translations/lb.json
index 80892a3e282419..92487c12ea6b09 100644
--- a/homeassistant/components/homematicip_cloud/translations/lb.json
+++ b/homeassistant/components/homematicip_cloud/translations/lb.json
@@ -6,6 +6,7 @@
"unknown": "Onbekannten Feeler opgetrueden"
},
"error": {
+ "invalid_pin": "Ong\u00ebltege Pin, prob\u00e9ier w.e.g. nach emol.",
"invalid_sgtin_or_pin": "Ong\u00ebltege Pin, prob\u00e9iert w.e.g. nach emol.",
"press_the_button": "Dr\u00e9ckt w.e.g. de bloe Kn\u00e4ppchen.",
"register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol.",
diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json
index 7127b5c5aae98e..f16e385c3a0908 100644
--- a/homeassistant/components/homematicip_cloud/translations/nl.json
+++ b/homeassistant/components/homematicip_cloud/translations/nl.json
@@ -6,6 +6,7 @@
"unknown": "Er is een onbekende fout opgetreden."
},
"error": {
+ "invalid_pin": "Ongeldige pincode, probeer het opnieuw.",
"invalid_sgtin_or_pin": "Ongeldige PIN-code, probeer het nogmaals.",
"press_the_button": "Druk op de blauwe knop.",
"register_failed": "Kan niet registreren, gelieve opnieuw te proberen.",
diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json
index cfd8e96c2a2fa5..c317bbddd26c0a 100644
--- a/homeassistant/components/homematicip_cloud/translations/pl.json
+++ b/homeassistant/components/homematicip_cloud/translations/pl.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.",
"connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.",
diff --git a/homeassistant/components/homematicip_cloud/translations/ru.json b/homeassistant/components/homematicip_cloud/translations/ru.json
index b307140ef02a21..a7b3ad1506c19f 100644
--- a/homeassistant/components/homematicip_cloud/translations/ru.json
+++ b/homeassistant/components/homematicip_cloud/translations/ru.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ "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.",
+ "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index 29db6026cc4c1c..8deed67e62bea5 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -63,6 +63,7 @@
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
KEY_DIALUP_MOBILE_DATASWITCH,
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
KEY_MONITORING_MONTH_STATISTICS,
KEY_MONITORING_STATUS,
KEY_MONITORING_TRAFFIC_STATISTICS,
@@ -145,7 +146,7 @@ class Router:
suspended = attr.ib(init=False, default=False)
notify_last_attempt: float = attr.ib(init=False, default=-1)
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Set up internal state on init."""
self.client = Client(self.connection)
@@ -175,7 +176,7 @@ def device_connections(self) -> Set[Tuple[str, str]]:
"""Get router connections for device registry."""
return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set()
- def _get_data(self, key: str, func: Callable[[None], Any]) -> None:
+ def _get_data(self, key: str, func: Callable[[], Any]) -> None:
if not self.subscriptions.get(key):
return
if key in self.inflight_gets:
@@ -243,6 +244,10 @@ def update(self) -> None:
self._get_data(
KEY_MONITORING_MONTH_STATISTICS, self.client.monitoring.month_statistics
)
+ self._get_data(
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
+ self.client.monitoring.check_notifications,
+ )
self._get_data(KEY_MONITORING_STATUS, self.client.monitoring.status)
self._get_data(
KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics
@@ -270,7 +275,7 @@ def logout(self) -> None:
except Exception: # pylint: disable=broad-except
_LOGGER.warning("Logout error", exc_info=True)
- def cleanup(self, *_) -> None:
+ def cleanup(self, *_: Any) -> None:
"""Clean up resources."""
self.subscriptions.clear()
@@ -354,7 +359,7 @@ def get_connection() -> Connection:
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
if username or password:
- connection = AuthorizedConnection(
+ connection: Connection = AuthorizedConnection(
url, username=username, password=password, timeout=CONNECTION_TIMEOUT
)
else:
diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py
index 575cc9789ca50d..f5c60963aa7d3c 100644
--- a/homeassistant/components/huawei_lte/binary_sensor.py
+++ b/homeassistant/components/huawei_lte/binary_sensor.py
@@ -1,7 +1,7 @@
"""Support for Huawei LTE binary sensors."""
import logging
-from typing import Optional
+from typing import List, Optional
import attr
from huawei_lte_api.enums.cradle import ConnectionStatusEnum
@@ -11,9 +11,15 @@
BinarySensorEntity,
)
from homeassistant.const import CONF_URL
+from homeassistant.helpers.entity import Entity
from . import HuaweiLteBaseEntity
-from .const import DOMAIN, KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH
+from .const import (
+ DOMAIN,
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
+ KEY_MONITORING_STATUS,
+ KEY_WLAN_WIFI_FEATURE_SWITCH,
+)
_LOGGER = logging.getLogger(__name__)
@@ -21,7 +27,7 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- entities = []
+ entities: List[Entity] = []
if router.data.get(KEY_MONITORING_STATUS):
entities.append(HuaweiLteMobileConnectionBinarySensor(router))
@@ -29,6 +35,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(HuaweiLteWifi24ghzStatusBinarySensor(router))
entities.append(HuaweiLteWifi5ghzStatusBinarySensor(router))
+ if router.data.get(KEY_MONITORING_CHECK_NOTIFICATIONS):
+ entities.append(HuaweiLteSmsStorageFullBinarySensor(router))
+
async_add_entities(entities, True)
@@ -49,19 +58,19 @@ def entity_registry_enabled_default(self) -> bool:
def _device_unique_id(self) -> str:
return f"{self.key}.{self.item}"
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Subscribe to needed data on add."""
await super().async_added_to_hass()
self.router.subscriptions[self.key].add(f"{BINARY_SENSOR_DOMAIN}/{self.item}")
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from needed data on remove."""
await super().async_will_remove_from_hass()
self.router.subscriptions[self.key].remove(
f"{BINARY_SENSOR_DOMAIN}/{self.item}"
)
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update state."""
try:
value = self.router.data[self.key][self.item]
@@ -86,7 +95,7 @@ async def async_update(self):
class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor):
"""Huawei LTE mobile connection binary sensor."""
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize identifiers."""
self.key = KEY_MONITORING_STATUS
self.item = "ConnectionStatus"
@@ -113,7 +122,7 @@ def assumed_state(self) -> bool:
)
@property
- def icon(self):
+ def icon(self) -> str:
"""Return mobile connectivity sensor icon."""
return "mdi:signal" if self.is_on else "mdi:signal-off"
@@ -149,7 +158,7 @@ def assumed_state(self) -> bool:
return self._raw_state is None
@property
- def icon(self):
+ def icon(self) -> str:
"""Return WiFi status sensor icon."""
return "mdi:wifi" if self.is_on else "mdi:wifi-off"
@@ -158,7 +167,7 @@ def icon(self):
class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor):
"""Huawei LTE WiFi status binary sensor."""
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize identifiers."""
self.key = KEY_MONITORING_STATUS
self.item = "WifiStatus"
@@ -172,7 +181,7 @@ def _entity_name(self) -> str:
class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor):
"""Huawei LTE 2.4GHz WiFi status binary sensor."""
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize identifiers."""
self.key = KEY_WLAN_WIFI_FEATURE_SWITCH
self.item = "wifi24g_switch_enable"
@@ -186,7 +195,7 @@ def _entity_name(self) -> str:
class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor):
"""Huawei LTE 5GHz WiFi status binary sensor."""
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize identifiers."""
self.key = KEY_WLAN_WIFI_FEATURE_SWITCH
self.item = "wifi5g_enabled"
@@ -194,3 +203,32 @@ def __attrs_post_init__(self):
@property
def _entity_name(self) -> str:
return "5GHz WiFi status"
+
+
+@attr.s
+class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor):
+ """Huawei LTE SMS storage full binary sensor."""
+
+ def __attrs_post_init__(self) -> None:
+ """Initialize identifiers."""
+ self.key = KEY_MONITORING_CHECK_NOTIFICATIONS
+ self.item = "SmsStorageFull"
+
+ @property
+ def _entity_name(self) -> str:
+ return "SMS storage full"
+
+ @property
+ def is_on(self) -> bool:
+ """Return whether the binary sensor is on."""
+ return self._raw_state is not None and int(self._raw_state) != 0
+
+ @property
+ def assumed_state(self) -> bool:
+ """Return True if real state is assumed, not known."""
+ return self._raw_state is None
+
+ @property
+ def icon(self) -> str:
+ """Return WiFi status sensor icon."""
+ return "mdi:email-alert" if self.is_on else "mdi:email-off"
diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py
index 9d5ce40074d3ba..af45f7e59bd1ef 100644
--- a/homeassistant/components/huawei_lte/config_flow.py
+++ b/homeassistant/components/huawei_lte/config_flow.py
@@ -121,7 +121,7 @@ async def async_step_user(self, user_input=None):
if self._already_configured(user_input):
return self.async_abort(reason="already_configured")
- conn = None
+ conn: Optional[Connection] = None
def logout():
if hasattr(conn, "user"):
@@ -189,7 +189,7 @@ def get_router_title(conn: Connection) -> str:
except LoginErrorPasswordWrongException:
errors[CONF_PASSWORD] = "incorrect_password"
except LoginErrorUsernamePasswordWrongException:
- errors[CONF_USERNAME] = "incorrect_username_or_password"
+ errors[CONF_USERNAME] = "invalid_auth"
except LoginErrorUsernamePasswordOverrunException:
errors["base"] = "login_attempts_exceeded"
except ResponseErrorException:
@@ -200,7 +200,7 @@ def get_router_title(conn: Connection) -> str:
errors[CONF_URL] = "connection_timeout"
except Exception: # pylint: disable=broad-except
_LOGGER.warning("Unknown error connecting to device", exc_info=True)
- errors[CONF_URL] = "unknown_connection_error"
+ errors[CONF_URL] = "unknown"
if errors:
await self.hass.async_add_executor_job(logout)
return await self._async_show_user_form(
diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py
index 2c5a3f8a9f66a8..039bab10fb95e6 100644
--- a/homeassistant/components/huawei_lte/const.py
+++ b/homeassistant/components/huawei_lte/const.py
@@ -27,6 +27,7 @@
KEY_DEVICE_INFORMATION = "device_information"
KEY_DEVICE_SIGNAL = "device_signal"
KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch"
+KEY_MONITORING_CHECK_NOTIFICATIONS = "monitoring_check_notifications"
KEY_MONITORING_MONTH_STATISTICS = "monitoring_month_statistics"
KEY_MONITORING_STATUS = "monitoring_status"
KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics"
@@ -36,13 +37,18 @@
KEY_WLAN_HOST_LIST = "wlan_host_list"
KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch"
-BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH}
+BINARY_SENSOR_KEYS = {
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
+ KEY_MONITORING_STATUS,
+ KEY_WLAN_WIFI_FEATURE_SWITCH,
+}
DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST}
SENSOR_KEYS = {
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
KEY_MONITORING_MONTH_STATISTICS,
KEY_MONITORING_STATUS,
KEY_MONITORING_TRAFFIC_STATISTICS,
diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py
index 54e8f318cf68b9..781f2dfcf11cbc 100644
--- a/homeassistant/components/huawei_lte/device_tracker.py
+++ b/homeassistant/components/huawei_lte/device_tracker.py
@@ -16,6 +16,7 @@
from homeassistant.core import callback
from homeassistant.helpers import entity_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
from . import HuaweiLteBaseEntity
from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL
@@ -81,7 +82,7 @@ def async_add_new_entities(hass, router_url, async_add_entities, tracked):
_LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host")
return
- new_entities = []
+ new_entities: List[Entity] = []
for host in (x for x in hosts if x.get("MacAddress")):
entity = HuaweiLteScannerEntity(router, host["MacAddress"])
if entity.unique_id in tracked:
@@ -116,7 +117,7 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
_hostname: Optional[str] = attr.ib(init=False, default=None)
_device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict)
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize internal state."""
self._device_state_attributes["mac_address"] = self.mac
@@ -148,17 +149,8 @@ async def async_update(self) -> None:
hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"]
host = next((x for x in hosts if x.get("MacAddress") == self.mac), None)
self._is_connected = host is not None
- if self._is_connected:
+ if host is not None:
self._hostname = host.get("HostName")
self._device_state_attributes = {
_better_snakecase(k): v for k, v in host.items() if k != "HostName"
}
-
-
-def get_scanner(*args, **kwargs): # pylint: disable=useless-return
- """Old no longer used way to set up Huawei LTE device tracker."""
- _LOGGER.warning(
- "Loading and configuring as a platform is no longer supported or "
- "required, convert to enabling/disabling available entities"
- )
- return None
diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py
index 91cc8864eb0c9f..375ced911c8d2d 100644
--- a/homeassistant/components/huawei_lte/notify.py
+++ b/homeassistant/components/huawei_lte/notify.py
@@ -19,10 +19,6 @@
async def async_get_service(hass, config, discovery_info=None):
"""Get the notification service."""
if discovery_info is None:
- _LOGGER.warning(
- "Loading as a platform is no longer supported, convert to use "
- "config entries or the huawei_lte component"
- )
return None
router = hass.data[DOMAIN].routers[discovery_info[CONF_URL]]
diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py
index f547dbd2eb6a61..64f5f8176a9b63 100644
--- a/homeassistant/components/huawei_lte/sensor.py
+++ b/homeassistant/components/huawei_lte/sensor.py
@@ -2,7 +2,7 @@
import logging
import re
-from typing import Optional
+from typing import Callable, Dict, List, NamedTuple, Optional, Pattern, Tuple, Union
import attr
@@ -10,13 +10,22 @@
DEVICE_CLASS_SIGNAL_STRENGTH,
DOMAIN as SENSOR_DOMAIN,
)
-from homeassistant.const import CONF_URL, DATA_BYTES, STATE_UNKNOWN, TIME_SECONDS
+from homeassistant.const import (
+ CONF_URL,
+ DATA_BYTES,
+ DATA_RATE_BYTES_PER_SECOND,
+ STATE_UNKNOWN,
+ TIME_SECONDS,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import StateType
from . import HuaweiLteBaseEntity
from .const import (
DOMAIN,
KEY_DEVICE_INFORMATION,
KEY_DEVICE_SIGNAL,
+ KEY_MONITORING_CHECK_NOTIFICATIONS,
KEY_MONITORING_MONTH_STATISTICS,
KEY_MONITORING_STATUS,
KEY_MONITORING_TRAFFIC_STATISTICS,
@@ -29,25 +38,66 @@
_LOGGER = logging.getLogger(__name__)
-SENSOR_META = {
- KEY_DEVICE_INFORMATION: dict(
+class SensorMeta(NamedTuple):
+ """Metadata for defining sensors."""
+
+ name: Optional[str] = None
+ device_class: Optional[str] = None
+ icon: Union[str, Callable[[StateType], str], None] = None
+ unit: Optional[str] = None
+ enabled_default: bool = False
+ include: Optional[Pattern[str]] = None
+ exclude: Optional[Pattern[str]] = None
+ formatter: Optional[Callable[[str], Tuple[StateType, Optional[str]]]] = None
+
+
+SENSOR_META: Dict[Union[str, Tuple[str, str]], SensorMeta] = {
+ KEY_DEVICE_INFORMATION: SensorMeta(
include=re.compile(r"^WanIP.*Address$", re.IGNORECASE)
),
- (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict(
+ (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta(
name="WAN IP address", icon="mdi:ip", enabled_default=True
),
- (KEY_DEVICE_INFORMATION, "WanIPv6Address"): dict(
+ (KEY_DEVICE_INFORMATION, "WanIPv6Address"): SensorMeta(
name="WAN IPv6 address", icon="mdi:ip"
),
- (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"),
- (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"),
- (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC", icon="mdi:map-marker"),
- (KEY_DEVICE_SIGNAL, "mode"): dict(
+ (KEY_DEVICE_SIGNAL, "band"): SensorMeta(name="Band"),
+ (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta(name="Cell ID"),
+ (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta(name="Downlink MCS"),
+ (KEY_DEVICE_SIGNAL, "dlbandwidth"): SensorMeta(
+ name="Downlink bandwidth",
+ icon=lambda x: (x is None or x < 8)
+ and "mdi:speedometer-slow"
+ or x < 15
+ and "mdi:speedometer-medium"
+ or "mdi:speedometer",
+ ),
+ (KEY_DEVICE_SIGNAL, "earfcn"): SensorMeta(name="EARFCN"),
+ (KEY_DEVICE_SIGNAL, "lac"): SensorMeta(name="LAC", icon="mdi:map-marker"),
+ (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta(name="PLMN"),
+ (KEY_DEVICE_SIGNAL, "rac"): SensorMeta(name="RAC", icon="mdi:map-marker"),
+ (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta(name="RRC status"),
+ (KEY_DEVICE_SIGNAL, "tac"): SensorMeta(name="TAC", icon="mdi:map-marker"),
+ (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta(name="TDD"),
+ (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta(
+ name="Transmit power",
+ device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
+ ),
+ (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta(name="Uplink MCS"),
+ (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta(
+ name="Uplink bandwidth",
+ icon=lambda x: (x is None or x < 8)
+ and "mdi:speedometer-slow"
+ or x < 15
+ and "mdi:speedometer-medium"
+ or "mdi:speedometer",
+ ),
+ (KEY_DEVICE_SIGNAL, "mode"): SensorMeta(
name="Mode",
formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None),
),
- (KEY_DEVICE_SIGNAL, "pci"): dict(name="PCI"),
- (KEY_DEVICE_SIGNAL, "rsrq"): dict(
+ (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI"),
+ (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta(
name="RSRQ",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# http://www.lte-anbieter.info/technik/rsrq.php
@@ -60,7 +110,7 @@
or "mdi:signal-cellular-3",
enabled_default=True,
),
- (KEY_DEVICE_SIGNAL, "rsrp"): dict(
+ (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta(
name="RSRP",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# http://www.lte-anbieter.info/technik/rsrp.php
@@ -73,7 +123,7 @@
or "mdi:signal-cellular-3",
enabled_default=True,
),
- (KEY_DEVICE_SIGNAL, "rssi"): dict(
+ (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta(
name="RSSI",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# https://eyesaas.com/wi-fi-signal-strength/
@@ -86,7 +136,7 @@
or "mdi:signal-cellular-3",
enabled_default=True,
),
- (KEY_DEVICE_SIGNAL, "sinr"): dict(
+ (KEY_DEVICE_SIGNAL, "sinr"): SensorMeta(
name="SINR",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# http://www.lte-anbieter.info/technik/sinr.php
@@ -99,7 +149,7 @@
or "mdi:signal-cellular-3",
enabled_default=True,
),
- (KEY_DEVICE_SIGNAL, "rscp"): dict(
+ (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta(
name="RSCP",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# https://wiki.teltonika.lt/view/RSCP
@@ -111,7 +161,7 @@
and "mdi:signal-cellular-2"
or "mdi:signal-cellular-3",
),
- (KEY_DEVICE_SIGNAL, "ecio"): dict(
+ (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta(
name="EC/IO",
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
# https://wiki.teltonika.lt/view/EC/IO
@@ -123,69 +173,90 @@
and "mdi:signal-cellular-2"
or "mdi:signal-cellular-3",
),
- KEY_MONITORING_MONTH_STATISTICS: dict(
+ KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta(
+ exclude=re.compile(
+ r"^(onlineupdatestatus|smsstoragefull)$",
+ re.IGNORECASE,
+ )
+ ),
+ (KEY_MONITORING_CHECK_NOTIFICATIONS, "UnreadMessage"): SensorMeta(
+ name="SMS unread", icon="mdi:email-receive"
+ ),
+ KEY_MONITORING_MONTH_STATISTICS: SensorMeta(
exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE)
),
- (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthDownload"): dict(
+ (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthDownload"): SensorMeta(
name="Current month download", unit=DATA_BYTES, icon="mdi:download"
),
- (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthUpload"): dict(
+ (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthUpload"): SensorMeta(
name="Current month upload", unit=DATA_BYTES, icon="mdi:upload"
),
- KEY_MONITORING_STATUS: dict(
+ KEY_MONITORING_STATUS: SensorMeta(
include=re.compile(
r"^(currentwifiuser|(primary|secondary).*dns)$", re.IGNORECASE
)
),
- (KEY_MONITORING_STATUS, "CurrentWifiUser"): dict(
+ (KEY_MONITORING_STATUS, "CurrentWifiUser"): SensorMeta(
name="WiFi clients connected", icon="mdi:wifi"
),
- (KEY_MONITORING_STATUS, "PrimaryDns"): dict(
+ (KEY_MONITORING_STATUS, "PrimaryDns"): SensorMeta(
name="Primary DNS server", icon="mdi:ip"
),
- (KEY_MONITORING_STATUS, "SecondaryDns"): dict(
+ (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta(
name="Secondary DNS server", icon="mdi:ip"
),
- (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): dict(
+ (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta(
name="Primary IPv6 DNS server", icon="mdi:ip"
),
- (KEY_MONITORING_STATUS, "SecondaryIPv6Dns"): dict(
+ (KEY_MONITORING_STATUS, "SecondaryIPv6Dns"): SensorMeta(
name="Secondary IPv6 DNS server", icon="mdi:ip"
),
- KEY_MONITORING_TRAFFIC_STATISTICS: dict(
+ KEY_MONITORING_TRAFFIC_STATISTICS: SensorMeta(
exclude=re.compile(r"^showtraffic$", re.IGNORECASE)
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): SensorMeta(
name="Current connection duration", unit=TIME_SECONDS, icon="mdi:timer-outline"
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): SensorMeta(
name="Current connection download", unit=DATA_BYTES, icon="mdi:download"
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownloadRate"): SensorMeta(
+ name="Current download rate",
+ unit=DATA_RATE_BYTES_PER_SECOND,
+ icon="mdi:download",
+ ),
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): SensorMeta(
name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload"
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUploadRate"): SensorMeta(
+ name="Current upload rate",
+ unit=DATA_RATE_BYTES_PER_SECOND,
+ icon="mdi:upload",
+ ),
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): SensorMeta(
name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline"
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): SensorMeta(
name="Total download", unit=DATA_BYTES, icon="mdi:download"
),
- (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict(
+ (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): SensorMeta(
name="Total upload", unit=DATA_BYTES, icon="mdi:upload"
),
- KEY_NET_CURRENT_PLMN: dict(exclude=re.compile(r"^(Rat|ShortName)$", re.IGNORECASE)),
- (KEY_NET_CURRENT_PLMN, "State"): dict(
+ KEY_NET_CURRENT_PLMN: SensorMeta(
+ exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE)
+ ),
+ (KEY_NET_CURRENT_PLMN, "State"): SensorMeta(
name="Operator search mode",
formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None),
),
- (KEY_NET_CURRENT_PLMN, "FullName"): dict(
+ (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta(
name="Operator name",
),
- (KEY_NET_CURRENT_PLMN, "Numeric"): dict(
+ (KEY_NET_CURRENT_PLMN, "Numeric"): SensorMeta(
name="Operator code",
),
- KEY_NET_NET_MODE: dict(include=re.compile(r"^NetworkMode$", re.IGNORECASE)),
- (KEY_NET_NET_MODE, "NetworkMode"): dict(
+ KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)),
+ (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta(
name="Preferred mode",
formatter=lambda x: (
{
@@ -200,8 +271,52 @@
None,
),
),
- (KEY_SMS_SMS_COUNT, "LocalUnread"): dict(
- name="SMS unread",
+ (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta(
+ name="SMS deleted (device)",
+ icon="mdi:email-minus",
+ ),
+ (KEY_SMS_SMS_COUNT, "LocalDraft"): SensorMeta(
+ name="SMS drafts (device)",
+ icon="mdi:email-send-outline",
+ ),
+ (KEY_SMS_SMS_COUNT, "LocalInbox"): SensorMeta(
+ name="SMS inbox (device)",
+ icon="mdi:email",
+ ),
+ (KEY_SMS_SMS_COUNT, "LocalMax"): SensorMeta(
+ name="SMS capacity (device)",
+ icon="mdi:email",
+ ),
+ (KEY_SMS_SMS_COUNT, "LocalOutbox"): SensorMeta(
+ name="SMS outbox (device)",
+ icon="mdi:email-send",
+ ),
+ (KEY_SMS_SMS_COUNT, "LocalUnread"): SensorMeta(
+ name="SMS unread (device)",
+ icon="mdi:email-receive",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimDraft"): SensorMeta(
+ name="SMS drafts (SIM)",
+ icon="mdi:email-send-outline",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimInbox"): SensorMeta(
+ name="SMS inbox (SIM)",
+ icon="mdi:email",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimMax"): SensorMeta(
+ name="SMS capacity (SIM)",
+ icon="mdi:email",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimOutbox"): SensorMeta(
+ name="SMS outbox (SIM)",
+ icon="mdi:email-send",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimUnread"): SensorMeta(
+ name="SMS unread (SIM)",
+ icon="mdi:email-receive",
+ ),
+ (KEY_SMS_SMS_COUNT, "SimUsed"): SensorMeta(
+ name="SMS messages (SIM)",
icon="mdi:email-receive",
),
}
@@ -210,22 +325,22 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- sensors = []
+ sensors: List[Entity] = []
for key in SENSOR_KEYS:
items = router.data.get(key)
if not items:
continue
key_meta = SENSOR_META.get(key)
if key_meta:
- include = key_meta.get("include")
- if include:
- items = filter(include.search, items)
- exclude = key_meta.get("exclude")
- if exclude:
- items = [x for x in items if not exclude.search(x)]
+ if key_meta.include:
+ items = filter(key_meta.include.search, items)
+ if key_meta.exclude:
+ items = [x for x in items if not key_meta.exclude.search(x)]
for item in items:
sensors.append(
- HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {}))
+ HuaweiLteSensor(
+ router, key, item, SENSOR_META.get((key, item), SensorMeta())
+ )
)
async_add_entities(sensors, True)
@@ -254,48 +369,48 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
key: str = attr.ib()
item: str = attr.ib()
- meta: dict = attr.ib()
+ meta: SensorMeta = attr.ib()
- _state = attr.ib(init=False, default=STATE_UNKNOWN)
- _unit: str = attr.ib(init=False)
+ _state: StateType = attr.ib(init=False, default=STATE_UNKNOWN)
+ _unit: Optional[str] = attr.ib(init=False)
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Subscribe to needed data on add."""
await super().async_added_to_hass()
self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}")
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from needed data on remove."""
await super().async_will_remove_from_hass()
self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}")
@property
def _entity_name(self) -> str:
- return self.meta.get("name", self.item)
+ return self.meta.name or self.item
@property
def _device_unique_id(self) -> str:
return f"{self.key}.{self.item}"
@property
- def state(self):
+ def state(self) -> StateType:
"""Return sensor state."""
return self._state
@property
def device_class(self) -> Optional[str]:
"""Return sensor device class."""
- return self.meta.get("device_class")
+ return self.meta.device_class
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> Optional[str]:
"""Return sensor's unit of measurement."""
- return self.meta.get("unit", self._unit)
+ return self.meta.unit or self._unit
@property
- def icon(self):
+ def icon(self) -> Optional[str]:
"""Return icon for sensor."""
- icon = self.meta.get("icon")
+ icon = self.meta.icon
if callable(icon):
return icon(self.state)
return icon
@@ -303,9 +418,9 @@ def icon(self):
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
- return bool(self.meta.get("enabled_default"))
+ return self.meta.enabled_default
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update state."""
try:
value = self.router.data[self.key][self.item]
@@ -315,16 +430,8 @@ async def async_update(self):
return
self._available = True
- formatter = self.meta.get("formatter")
+ formatter = self.meta.formatter
if not callable(formatter):
formatter = format_default
self._state, self._unit = formatter(value)
-
-
-async def async_setup_platform(*args, **kwargs):
- """Old no longer used way to set up Huawei LTE sensors."""
- _LOGGER.warning(
- "Loading and configuring as a platform is no longer supported or "
- "required, convert to enabling/disabling available entities"
- )
diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json
index 8435d0a5347b91..00994f8b0a0ab1 100644
--- a/homeassistant/components/huawei_lte/strings.json
+++ b/homeassistant/components/huawei_lte/strings.json
@@ -1,20 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "This device has already been configured",
- "already_in_progress": "This device is already being configured",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"not_huawei_lte": "Not a Huawei LTE device"
},
"error": {
- "connection_failed": "Connection failed",
"connection_timeout": "Connection timeout",
"incorrect_password": "Incorrect password",
"incorrect_username": "Incorrect username",
- "incorrect_username_or_password": "Incorrect username or password",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_url": "Invalid URL",
"login_attempts_exceeded": "Maximum login attempts exceeded, please try again later",
"response_error": "Unknown error from device",
- "unknown_connection_error": "Unknown error connecting to device"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "Huawei LTE: {name}",
"step": {
diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py
index 45b179f470fc41..853fe3f40e7f8f 100644
--- a/homeassistant/components/huawei_lte/switch.py
+++ b/homeassistant/components/huawei_lte/switch.py
@@ -1,7 +1,7 @@
"""Support for Huawei LTE switches."""
import logging
-from typing import Optional
+from typing import Any, List, Optional
import attr
@@ -11,6 +11,7 @@
SwitchEntity,
)
from homeassistant.const import CONF_URL
+from homeassistant.helpers.entity import Entity
from . import HuaweiLteBaseEntity
from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH
@@ -21,7 +22,7 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- switches = []
+ switches: List[Entity] = []
if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH):
switches.append(HuaweiLteMobileDataSwitch(router))
@@ -40,30 +41,30 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity):
def _turn(self, state: bool) -> None:
raise NotImplementedError
- def turn_on(self, **kwargs):
+ def turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
self._turn(state=True)
- def turn_off(self, **kwargs):
+ def turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
self._turn(state=False)
@property
- def device_class(self):
+ def device_class(self) -> str:
"""Return device class."""
return DEVICE_CLASS_SWITCH
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Subscribe to needed data on add."""
await super().async_added_to_hass()
self.router.subscriptions[self.key].add(f"{SWITCH_DOMAIN}/{self.item}")
- async def async_will_remove_from_hass(self):
+ async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from needed data on remove."""
await super().async_will_remove_from_hass()
self.router.subscriptions[self.key].remove(f"{SWITCH_DOMAIN}/{self.item}")
- async def async_update(self):
+ async def async_update(self) -> None:
"""Update state."""
try:
value = self.router.data[self.key][self.item]
@@ -79,7 +80,7 @@ async def async_update(self):
class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch):
"""Huawei LTE mobile data switch device."""
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
"""Initialize identifiers."""
self.key = KEY_DIALUP_MOBILE_DATASWITCH
self.item = "dataswitch"
@@ -104,6 +105,6 @@ def _turn(self, state: bool) -> None:
self.schedule_update_ha_state()
@property
- def icon(self):
+ def icon(self) -> str:
"""Return switch icon."""
return "mdi:signal" if self.is_on else "mdi:signal-off"
diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json
index 1a7b245c9fefb2..856c3986ed0119 100644
--- a/homeassistant/components/huawei_lte/translations/ca.json
+++ b/homeassistant/components/huawei_lte/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu ja ha estat configurat",
- "already_in_progress": "Aquest dispositiu ja s'est\u00e0 configurant",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "Contrasenya incorrecta",
"incorrect_username": "Nom d'usuari incorrecte",
"incorrect_username_or_password": "Nom d'usuari o contrasenya incorrectes",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_url": "URL inv\u00e0lid",
"login_attempts_exceeded": "Nombre m\u00e0xim d'intents d'inici de sessi\u00f3 superat, torna-ho a provar m\u00e9s tard",
"response_error": "S'ha produ\u00eft un error desconegut del dispositiu",
+ "unknown": "Error inesperat",
"unknown_connection_error": "S'ha produ\u00eft un error desconegut en connectar-se al dispositiu"
},
"flow_title": "Huawei LTE: {name}",
diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json
index 15fd57a3d33603..a0fa9914c47435 100644
--- a/homeassistant/components/huawei_lte/translations/de.json
+++ b/homeassistant/components/huawei_lte/translations/de.json
@@ -16,10 +16,12 @@
"response_error": "Unbekannter Fehler vom Ger\u00e4t",
"unknown_connection_error": "Unbekannter Fehler beim Herstellen der Verbindung zum Ger\u00e4t"
},
+ "flow_title": "Huawei LTE: {name}",
"step": {
"user": {
"data": {
"password": "Passwort",
+ "url": "URL",
"username": "Benutzername"
},
"description": "Gib die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.",
diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json
new file mode 100644
index 00000000000000..0957ca085d79e9
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/el.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7",
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json
index 022328ea2eaf6b..fa66aebeba80f6 100644
--- a/homeassistant/components/huawei_lte/translations/en.json
+++ b/homeassistant/components/huawei_lte/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "This device has already been configured",
- "already_in_progress": "This device is already being configured",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"not_huawei_lte": "Not a Huawei LTE device"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "Incorrect password",
"incorrect_username": "Incorrect username",
"incorrect_username_or_password": "Incorrect username or password",
+ "invalid_auth": "Invalid authentication",
"invalid_url": "Invalid URL",
"login_attempts_exceeded": "Maximum login attempts exceeded, please try again later",
"response_error": "Unknown error from device",
+ "unknown": "Unexpected error",
"unknown_connection_error": "Unknown error connecting to device"
},
"flow_title": "Huawei LTE: {name}",
diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json
index 268fdf8eff57fc..289bd7c534c564 100644
--- a/homeassistant/components/huawei_lte/translations/es.json
+++ b/homeassistant/components/huawei_lte/translations/es.json
@@ -11,9 +11,11 @@
"incorrect_password": "Contrase\u00f1a incorrecta",
"incorrect_username": "Nombre de usuario incorrecto",
"incorrect_username_or_password": "Nombre de usuario o contrase\u00f1a incorrectos",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_url": "URL no v\u00e1lida",
"login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, int\u00e9ntelo de nuevo m\u00e1s tarde.",
"response_error": "Error desconocido del dispositivo",
+ "unknown": "Error inesperado",
"unknown_connection_error": "Error desconocido al conectarse al dispositivo"
},
"flow_title": "Huawei LTE: {name}",
diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json
new file mode 100644
index 00000000000000..8c8a4497b99edf
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json
index 2c7437d9a144c1..0662843a918ba3 100644
--- a/homeassistant/components/huawei_lte/translations/fr.json
+++ b/homeassistant/components/huawei_lte/translations/fr.json
@@ -11,9 +11,11 @@
"incorrect_password": "Mot de passe incorrect",
"incorrect_username": "Nom d'utilisateur incorrect",
"incorrect_username_or_password": "identifiant ou mot de passe incorrect",
+ "invalid_auth": "Authentification invalide",
"invalid_url": "URL invalide",
"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",
"unknown_connection_error": "Erreur inconnue lors de la connexion \u00e0 l'appareil"
},
"flow_title": "Huawei LTE: {nom}",
diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json
index 53a91afab06c14..cf6e97846d388b 100644
--- a/homeassistant/components/huawei_lte/translations/it.json
+++ b/homeassistant/components/huawei_lte/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato",
- "already_in_progress": "Questo dispositivo \u00e8 gi\u00e0 in fase di configurazione",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "Password errata",
"incorrect_username": "Nome utente errato",
"incorrect_username_or_password": "Nome utente o password errati",
+ "invalid_auth": "Autenticazione non valida",
"invalid_url": "URL non valido",
"login_attempts_exceeded": "Superati i tentativi di accesso massimi, riprovare pi\u00f9 tardi",
"response_error": "Errore sconosciuto dal dispositivo",
+ "unknown": "Errore imprevisto",
"unknown_connection_error": "Errore sconosciuto durante la connessione al dispositivo"
},
"flow_title": "Huawei LTE: {name}",
diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json
index 710a318d9667f6..f33b88622786ac 100644
--- a/homeassistant/components/huawei_lte/translations/no.json
+++ b/homeassistant/components/huawei_lte/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Denne enheten er allerede konfigurert",
- "already_in_progress": "Denne enheten blir allerede konfigurert",
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"not_huawei_lte": "Ikke en Huawei LTE-enhet"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "Feil passord",
"incorrect_username": "Feil brukernavn",
"incorrect_username_or_password": "Feil brukernavn eller passord",
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_url": "Ugyldig URL-adresse",
"login_attempts_exceeded": "Maksimalt antall p\u00e5loggingsfors\u00f8k er overskredet, vennligst pr\u00f8v igjen senere",
"response_error": "Ukjent feil fra enheten",
+ "unknown": "Uventet feil",
"unknown_connection_error": "Ukjent feil under tilkobling til enhet"
},
"flow_title": "",
diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json
index e38188d134f2f7..405ffdf0343d13 100644
--- a/homeassistant/components/huawei_lte/translations/pl.json
+++ b/homeassistant/components/huawei_lte/translations/pl.json
@@ -21,6 +21,7 @@
"user": {
"data": {
"password": "Has\u0142o",
+ "url": "URL",
"username": "Nazwa u\u017cytkownika"
},
"description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistanta gdy integracja jest aktywna.",
diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json
index 5830cb8ccb3ac0..68c025894cd0c5 100644
--- a/homeassistant/components/huawei_lte/translations/ru.json
+++ b/homeassistant/components/huawei_lte/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
"not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
"incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.",
"incorrect_username_or_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.",
"login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.",
"response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"unknown_connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"flow_title": "Huawei LTE: {name}",
diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json
index 55cc0b6acf6ccd..7dcc0f8851dcc6 100644
--- a/homeassistant/components/huawei_lte/translations/zh-Hant.json
+++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u8a2d\u5099"
},
"error": {
@@ -11,9 +11,11 @@
"incorrect_password": "\u5bc6\u78bc\u932f\u8aa4",
"incorrect_username": "\u4f7f\u7528\u8005\u540d\u7a31\u932f\u8aa4",
"incorrect_username_or_password": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_url": "\u7db2\u5740\u7121\u6548",
"login_attempts_exceeded": "\u5df2\u9054\u5617\u8a66\u767b\u5165\u6700\u5927\u6b21\u6578\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66",
"response_error": "\u4f86\u81ea\u8a2d\u5099\u672a\u77e5\u932f\u8aa4",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4",
"unknown_connection_error": "\u9023\u7dda\u81f3\u8a2d\u5099\u672a\u77e5\u932f\u8aa4"
},
"flow_title": "\u83ef\u70ba LTE\uff1a{name}",
diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py
index be34b26be0d4b3..69278ed6574a88 100644
--- a/homeassistant/components/huawei_router/device_tracker.py
+++ b/homeassistant/components/huawei_router/device_tracker.py
@@ -50,7 +50,7 @@ class HuaweiDeviceScanner(DeviceScanner):
'"(?P.*?)","(?P.*?)",'
'"(?P.*?)"'
)
- LOGIN_COOKIE = dict(Cookie="body:Language:portuguese:id=-1")
+ LOGIN_COOKIE = {"Cookie": "body:Language:portuguese:id=-1"}
def __init__(self, config):
"""Initialize the scanner."""
diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py
index 20066bd1be2e2f..821d482ec25dd9 100644
--- a/homeassistant/components/hue/light.py
+++ b/homeassistant/components/hue/light.py
@@ -458,7 +458,6 @@ async def async_turn_off(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attributes = {}
- if self.is_group:
- attributes[ATTR_IS_HUE_GROUP] = self.is_group
- return attributes
+ if not self.is_group:
+ return {}
+ return {ATTR_IS_HUE_GROUP: self.is_group}
diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py
index e96f844a5e10c9..f5911bbb50c7d3 100644
--- a/homeassistant/components/hue/sensor.py
+++ b/homeassistant/components/hue/sensor.py
@@ -10,6 +10,7 @@
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
)
@@ -41,7 +42,7 @@ class HueLightLevel(GenericHueGaugeSensorEntity):
"""The light level sensor entity for a Hue motion sensor device."""
device_class = DEVICE_CLASS_ILLUMINANCE
- unit_of_measurement = "lx"
+ unit_of_measurement = LIGHT_LUX
@property
def state(self):
diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json
index a16d5e3a632719..678b7c2cad24a8 100644
--- a/homeassistant/components/hue/strings.json
+++ b/homeassistant/components/hue/strings.json
@@ -20,16 +20,16 @@
},
"error": {
"register_failed": "Failed to register, please try again",
- "linking": "Unknown linking error occurred."
+ "linking": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
"all_configured": "All Philips Hue bridges are already configured",
- "unknown": "Unknown error occurred",
- "cannot_connect": "Unable to connect to the bridge",
- "already_configured": "Bridge is already configured",
- "already_in_progress": "Config flow for bridge is already in progress.",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"not_hue_bridge": "Not a Hue bridge"
}
},
diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json
index 97098b21db7768..47bb10b2abbdd5 100644
--- a/homeassistant/components/hue/translations/ca.json
+++ b/homeassistant/components/hue/translations/ca.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats",
- "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades de configuraci\u00f3 per l'enlla\u00e7 ja est\u00e0 en curs.",
- "cannot_connect": "No s'ha pogut connectar amb l'enlla\u00e7",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue",
"no_bridges": "No s'han trobat enlla\u00e7os Philips Hue",
"not_hue_bridge": "No \u00e9s un enlla\u00e7 Hue",
- "unknown": "S'ha produ\u00eft un error desconegut"
+ "unknown": "Error inesperat"
},
"error": {
- "linking": "S'ha produ\u00eft un error desconegut al vincular.",
+ "linking": "Error inesperat",
"register_failed": "No s'ha pogut registrar, torna-ho a provar"
},
"step": {
diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json
index c9c8c96f4d559d..0defb33ae5e509 100644
--- a/homeassistant/components/hue/translations/de.json
+++ b/homeassistant/components/hue/translations/de.json
@@ -26,6 +26,9 @@
"title": "Hub verbinden"
},
"manual": {
+ "data": {
+ "host": "Host"
+ },
"title": "Manuelles Konfigurieren einer Hue Bridge"
}
}
diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json
index 9f3a96f2da3580..e03eabd3d2361c 100644
--- a/homeassistant/components/hue/translations/en.json
+++ b/homeassistant/components/hue/translations/en.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "All Philips Hue bridges are already configured",
- "already_configured": "Bridge is already configured",
- "already_in_progress": "Config flow for bridge is already in progress.",
- "cannot_connect": "Unable to connect to the bridge",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "cannot_connect": "Failed to connect",
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
"not_hue_bridge": "Not a Hue bridge",
- "unknown": "Unknown error occurred"
+ "unknown": "Unexpected error"
},
"error": {
- "linking": "Unknown linking error occurred.",
+ "linking": "Unexpected error",
"register_failed": "Failed to register, please try again"
},
"step": {
diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json
index 92553c84cfe22d..0aeea7286d9ea0 100644
--- a/homeassistant/components/hue/translations/et.json
+++ b/homeassistant/components/hue/translations/et.json
@@ -1,14 +1,46 @@
{
"config": {
"abort": {
+ "all_configured": "K\u00f5ik Philips Hue sillad on juba konfigureeritud",
+ "discover_timeout": "Ei leia Philips Hue sildu",
+ "no_bridges": "Philips Hue sildu ei avastatud",
"unknown": "Ilmnes tundmatu viga"
},
+ "error": {
+ "linking": "Ilmnes tundmatu linkimist\u00f5rge.",
+ "register_failed": "Registreerimine nurjus. Proovige uuesti"
+ },
"step": {
"init": {
"data": {
"host": ""
- }
+ },
+ "title": "Valige Hue sild"
+ },
+ "link": {
+ "description": "Vajutage silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Esimene nupp",
+ "button_2": "Teine nupp",
+ "button_3": "Kolmas nupp",
+ "button_4": "Neljas nupp",
+ "dim_down": "H\u00e4marda",
+ "dim_up": "Tee heledamaks",
+ "double_buttons_1_3": "Esimene ja kolmas nupp",
+ "double_buttons_2_4": "Teine ja neljas nupp",
+ "turn_off": "L\u00fclita v\u00e4lja",
+ "turn_on": "L\u00fclita sisse"
+ },
+ "trigger_type": {
+ "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust",
+ "remote_button_short_press": "\"{subtype}\" nupp on vajutatud",
+ "remote_button_short_release": "\"{subtype}\" nupp vabastati",
+ "remote_double_button_long_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati p\u00e4rast pikka vajutust",
+ "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json
index 99e82f1a89b506..f19c5ec7a34b71 100644
--- a/homeassistant/components/hue/translations/fr.json
+++ b/homeassistant/components/hue/translations/fr.json
@@ -49,7 +49,9 @@
"trigger_type": {
"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_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"
}
},
"options": {
diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json
index 6fc4554b384270..f49e64b9b685ef 100644
--- a/homeassistant/components/hue/translations/it.json
+++ b/homeassistant/components/hue/translations/it.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "Tutti i bridge di Philips Hue sono gi\u00e0 configurati",
- "already_configured": "Il bridge \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.",
- "cannot_connect": "Impossibile connettersi al bridge",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "cannot_connect": "Impossibile connettersi",
"discover_timeout": "Impossibile trovare i bridge Hue",
"no_bridges": "Nessun bridge di Philips Hue trovato",
"not_hue_bridge": "Non \u00e8 un bridge Hue",
- "unknown": "Si \u00e8 verificato un errore"
+ "unknown": "Errore imprevisto"
},
"error": {
- "linking": "Si \u00e8 verificato un errore sconosciuto in fase di collegamento.",
+ "linking": "Errore imprevisto",
"register_failed": "Errore in fase di registrazione, riprova"
},
"step": {
diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json
index 2d51ee26452df7..1b699c6e8850df 100644
--- a/homeassistant/components/hue/translations/no.json
+++ b/homeassistant/components/hue/translations/no.json
@@ -3,7 +3,7 @@
"abort": {
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
"already_configured": "Bridge er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for bro p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"cannot_connect": "Kan ikke koble til Bridge",
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
"no_bridges": "Ingen Philips Hue Bridger oppdaget",
diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json
index 02dad0c3e5276b..6e2623d23ac380 100644
--- a/homeassistant/components/hue/translations/pl.json
+++ b/homeassistant/components/hue/translations/pl.json
@@ -1,18 +1,18 @@
{
"config": {
"abort": {
- "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane.",
- "already_configured": "Mostek jest ju\u017c skonfigurowany.",
+ "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
+ "already_configured": "Mostek jest ju\u017c skonfigurowany",
"already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem",
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
- "no_bridges": "Nie wykryto mostk\u00f3w Hue.",
+ "no_bridges": "Nie wykryto mostk\u00f3w Hue",
"not_hue_bridge": "To nie jest mostek Hue",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",
- "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie."
+ "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie"
},
"step": {
"init": {
diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json
index f302eeaa4732c5..d33a23e6179f3a 100644
--- a/homeassistant/components/hue/translations/ru.json
+++ b/homeassistant/components/hue/translations/ru.json
@@ -3,7 +3,7 @@
"abort": {
"all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.",
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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 \u043a \u0448\u043b\u044e\u0437\u0443.",
"discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json
index 2442ae30d10dd6..be69584951f8c8 100644
--- a/homeassistant/components/hue/translations/zh-Hant.json
+++ b/homeassistant/components/hue/translations/zh-Hant.json
@@ -3,7 +3,7 @@
"abort": {
"all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210",
"already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge",
"discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge",
"no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge",
diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py
index a6194994a9c03e..6bccd375207cc2 100644
--- a/homeassistant/components/humidifier/device_action.py
+++ b/homeassistant/components/humidifier/device_action.py
@@ -6,6 +6,7 @@
from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@@ -63,7 +64,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
if state is None:
continue
- if state.attributes["supported_features"] & const.SUPPORT_MODES:
+ if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES:
actions.append(
{
CONF_DEVICE_ID: device_id,
diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py
index 7f37fc3b1fa533..714a51ab0161dc 100644
--- a/homeassistant/components/humidifier/device_condition.py
+++ b/homeassistant/components/humidifier/device_condition.py
@@ -6,6 +6,7 @@
from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -48,7 +49,7 @@ async def async_get_conditions(
state = hass.states.get(entry.entity_id)
- if state and state.attributes["supported_features"] & const.SUPPORT_MODES:
+ if state and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES:
conditions.append(
{
CONF_CONDITION: "device",
diff --git a/homeassistant/components/humidifier/group.py b/homeassistant/components/humidifier/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/humidifier/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/humidifier/translations/et.json b/homeassistant/components/humidifier/translations/et.json
new file mode 100644
index 00000000000000..303edb781b68a1
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_humidity": "M\u00e4\u00e4ra {entity_name} niiskus",
+ "set_mode": "Muuda {entity_name} t\u00f6\u00f6re\u017eiimi",
+ "toggle": "Muuda {entity_name} olekut",
+ "turn_off": "L\u00fclita {entity_name} v\u00e4lja",
+ "turn_on": "L\u00fclita {entity_name} sisse"
+ },
+ "condition_type": {
+ "is_mode": "{entity_name} on seatud kindlale t\u00f6\u00f6re\u017eiimile",
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud"
+ },
+ "trigger_type": {
+ "target_humidity_changed": "{entity_name} eelseatud niiskus muutus",
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json
index 4b680bdba7fdbe..236c3b93343e68 100644
--- a/homeassistant/components/humidifier/translations/fr.json
+++ b/homeassistant/components/humidifier/translations/fr.json
@@ -8,10 +8,12 @@
"turn_on": "Allumer {entity_name}"
},
"condition_type": {
+ "is_mode": "{entity_name} est d\u00e9fini sur un mode sp\u00e9cifique",
"is_off": "{entity_name} est d\u00e9sactiv\u00e9",
"is_on": "{entity_name} est activ\u00e9"
},
"trigger_type": {
+ "target_humidity_changed": "{nom_de_l'entit\u00e9} changement de l'humidit\u00e9 cible",
"turned_off": "{entity_name} s'est \u00e9teint",
"turned_on": "{entity_name} s'est allum\u00e9"
}
diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json
new file mode 100644
index 00000000000000..4081c4e13fc281
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/uk.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/strings.json b/homeassistant/components/hunterdouglas_powerview/strings.json
index 4cba22b60fb4b2..5d0b042454d7d9 100644
--- a/homeassistant/components/hunterdouglas_powerview/strings.json
+++ b/homeassistant/components/hunterdouglas_powerview/strings.json
@@ -14,11 +14,11 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/et.json b/homeassistant/components/hunterdouglas_powerview/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json
index a1bd06078c6e15..e5208ebdd689f5 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/fr.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json
@@ -19,5 +19,6 @@
"title": "Connectez-vous au concentrateur PowerView"
}
}
- }
+ },
+ "title": "Hunter Douglas PowerView"
}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json
index cad41869ced4e9..87d7b8a915e5ed 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/pl.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"link": {
diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py
index 853ed9460c8c20..e003b25ea85a50 100644
--- a/homeassistant/components/hvv_departures/__init__.py
+++ b/homeassistant/components/hvv_departures/__init__.py
@@ -1,6 +1,7 @@
"""The HVV integration."""
import asyncio
+from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR
from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
@@ -10,7 +11,7 @@
from .const import DOMAIN
from .hub import GTIHub
-PLATFORMS = [DOMAIN_SENSOR]
+PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR]
async def async_setup(hass: HomeAssistant, config: dict):
diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py
new file mode 100644
index 00000000000000..7d19fcc8fdfcc1
--- /dev/null
+++ b/homeassistant/components/hvv_departures/binary_sensor.py
@@ -0,0 +1,201 @@
+"""Binary sensor platform for hvv_departures."""
+from datetime import timedelta
+import logging
+
+from aiohttp import ClientConnectorError
+import async_timeout
+from pygti.exceptions import InvalidAuth
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the binary_sensor platform."""
+ hub = hass.data[DOMAIN][entry.entry_id]
+ station_name = entry.data[CONF_STATION]["name"]
+ station = entry.data[CONF_STATION]
+
+ def get_elevator_entities_from_station_information(
+ station_name, station_information
+ ):
+ """Convert station information into a list of elevators."""
+ elevators = {}
+
+ if station_information is None:
+ return {}
+
+ for partial_station in station_information.get("partialStations", []):
+ for elevator in partial_station.get("elevators", []):
+
+ state = elevator.get("state") != "READY"
+ available = elevator.get("state") != "UNKNOWN"
+ label = elevator.get("label")
+ description = elevator.get("description")
+
+ if label is not None:
+ name = f"Elevator {label} at {station_name}"
+ else:
+ name = f"Unknown elevator at {station_name}"
+
+ if description is not None:
+ name += f" ({description})"
+
+ lines = elevator.get("lines")
+
+ idx = f"{station_name}-{label}-{lines}"
+
+ elevators[idx] = {
+ "state": state,
+ "name": name,
+ "available": available,
+ "attributes": {
+ "cabin_width": elevator.get("cabinWidth"),
+ "cabin_length": elevator.get("cabinLength"),
+ "door_width": elevator.get("doorWidth"),
+ "elevator_type": elevator.get("elevatorType"),
+ "button_type": elevator.get("buttonType"),
+ "cause": elevator.get("cause"),
+ "lines": lines,
+ ATTR_ATTRIBUTION: ATTRIBUTION,
+ },
+ }
+ return elevators
+
+ async def async_update_data():
+ """Fetch data from API endpoint.
+
+ This is the place to pre-process the data to lookup tables
+ so entities can quickly look up their data.
+ """
+
+ payload = {"station": station}
+
+ try:
+ async with async_timeout.timeout(10):
+ return get_elevator_entities_from_station_information(
+ station_name, await hub.gti.stationInformation(payload)
+ )
+ except InvalidAuth as err:
+ raise UpdateFailed(f"Authentication failed: {err}") from err
+ except ClientConnectorError as err:
+ raise UpdateFailed(f"Network not available: {err}") from err
+ except Exception as err: # pylint: disable=broad-except
+ raise UpdateFailed(f"Error occurred while fetching data: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name="hvv_departures.binary_sensor",
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(hours=1),
+ )
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_refresh()
+
+ async_add_entities(
+ HvvDepartureBinarySensor(coordinator, idx, entry)
+ for (idx, ent) in coordinator.data.items()
+ )
+
+
+class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity):
+ """HVVDepartureBinarySensor class."""
+
+ def __init__(self, coordinator, idx, config_entry):
+ """Initialize."""
+ super().__init__(coordinator)
+ self.coordinator = coordinator
+ self.idx = idx
+ self.config_entry = config_entry
+
+ @property
+ def is_on(self):
+ """Return entity state."""
+ return self.coordinator.data[self.idx]["state"]
+
+ @property
+ def should_poll(self):
+ """No need to poll. Coordinator notifies entity of updates."""
+ return False
+
+ @property
+ def available(self):
+ """Return if entity is available."""
+ return (
+ self.coordinator.last_update_success
+ and self.coordinator.data[self.idx]["available"]
+ )
+
+ @property
+ def device_info(self):
+ """Return the device info for this sensor."""
+ return {
+ "identifiers": {
+ (
+ DOMAIN,
+ self.config_entry.entry_id,
+ self.config_entry.data[CONF_STATION]["id"],
+ self.config_entry.data[CONF_STATION]["type"],
+ )
+ },
+ "name": f"Departures at {self.config_entry.data[CONF_STATION]['name']}",
+ "manufacturer": MANUFACTURER,
+ }
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self.coordinator.data[self.idx]["name"]
+
+ @property
+ def unique_id(self):
+ """Return a unique ID to use for this sensor."""
+ return self.idx
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_PROBLEM
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if not (
+ self.coordinator.last_update_success
+ and self.coordinator.data[self.idx]["available"]
+ ):
+ return None
+ return {
+ k: v
+ for k, v in self.coordinator.data[self.idx]["attributes"].items()
+ if v is not None
+ }
+
+ async def async_added_to_hass(self):
+ """When entity is added to hass."""
+ self.async_on_remove(
+ self.coordinator.async_add_listener(self.async_write_ha_state)
+ )
+
+ async def async_update(self):
+ """Update the entity.
+
+ Only used by the generic entity update service.
+ """
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/hvv_departures/strings.json b/homeassistant/components/hvv_departures/strings.json
index dfd6484f7d8933..c29ad6cc69411c 100644
--- a/homeassistant/components/hvv_departures/strings.json
+++ b/homeassistant/components/hvv_departures/strings.json
@@ -5,9 +5,9 @@
"user": {
"title": "Connect to the HVV API",
"data": {
- "host": "Host",
- "username": "Username",
- "password": "Password"
+ "host": "[%key:common::config_flow::data::host%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
}
},
"station": {
@@ -24,12 +24,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_results": "No results. Try with a different station/address"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
diff --git a/homeassistant/components/hvv_departures/translations/ca.json b/homeassistant/components/hvv_departures/translations/ca.json
index 4b295512febdbb..7a6025459c95ca 100644
--- a/homeassistant/components/hvv_departures/translations/ca.json
+++ b/homeassistant/components/hvv_departures/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"no_results": "Sense resultats. Prova-ho amb una altra estaci\u00f3/adre\u00e7a"
},
diff --git a/homeassistant/components/hvv_departures/translations/de.json b/homeassistant/components/hvv_departures/translations/de.json
new file mode 100644
index 00000000000000..b383e57bd93fa3
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/de.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen, bitte erneut versuchen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "no_results": "Keine Ergebnisse. Versuch es mit einer anderen Station/Adresse"
+ },
+ "step": {
+ "station": {
+ "data": {
+ "station": "Station/Adresse"
+ },
+ "title": "Station/Adresse eingeben"
+ },
+ "station_select": {
+ "data": {
+ "station": "Station/Adresse"
+ },
+ "title": "Station/Adresse ausw\u00e4hlen"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "title": "Mit der HVV-API verbinden"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "filter": "Linien ausw\u00e4hlen",
+ "offset": "Versatz (Minuten)",
+ "real_time": "Echtzeitdaten verwenden"
+ },
+ "description": "Optionen f\u00fcr diesen Abfahrtssensor \u00e4ndern",
+ "title": "Optionen"
+ }
+ }
+ },
+ "title": "HVV Abfahrten"
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/en.json b/homeassistant/components/hvv_departures/translations/en.json
index ede3ece2f4a575..846457196e5a54 100644
--- a/homeassistant/components/hvv_departures/translations/en.json
+++ b/homeassistant/components/hvv_departures/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"no_results": "No results. Try with a different station/address"
},
diff --git a/homeassistant/components/hvv_departures/translations/fr.json b/homeassistant/components/hvv_departures/translations/fr.json
index afc67b1087d3e2..e6560da40471e7 100644
--- a/homeassistant/components/hvv_departures/translations/fr.json
+++ b/homeassistant/components/hvv_departures/translations/fr.json
@@ -35,11 +35,14 @@
"step": {
"init": {
"data": {
+ "filter": "S\u00e9lectionnez des lignes",
"offset": "D\u00e9calage (minutes)",
"real_time": "Utiliser des donn\u00e9es en temps r\u00e9el"
},
+ "description": "Modifier les options de ce capteur de d\u00e9part",
"title": "Options"
}
}
- }
+ },
+ "title": "D\u00e9parts HVV"
}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/it.json b/homeassistant/components/hvv_departures/translations/it.json
index f1dd507a0d5c44..2b9419f70cc18c 100644
--- a/homeassistant/components/hvv_departures/translations/it.json
+++ b/homeassistant/components/hvv_departures/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"no_results": "Nessun risultato. Prova con un'altra stazione/indirizzo"
},
diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/no.json b/homeassistant/components/hvv_departures/translations/no.json
index 52b91ef31d40dd..7d2ca8c5879650 100644
--- a/homeassistant/components/hvv_departures/translations/no.json
+++ b/homeassistant/components/hvv_departures/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"no_results": "Ingen resultater. Pr\u00f8v med en annen stasjon/adresse"
},
diff --git a/homeassistant/components/hvv_departures/translations/pl.json b/homeassistant/components/hvv_departures/translations/pl.json
index 5bf87fc08a8327..7ea22e48d5490f 100644
--- a/homeassistant/components/hvv_departures/translations/pl.json
+++ b/homeassistant/components/hvv_departures/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"no_results": "Brak wynik\u00f3w. Spr\u00f3buj z inn\u0105 stacj\u0105/adresem."
},
"step": {
diff --git a/homeassistant/components/hvv_departures/translations/ru.json b/homeassistant/components/hvv_departures/translations/ru.json
index b83981ae76ffa7..354faf167a72de 100644
--- a/homeassistant/components/hvv_departures/translations/ru.json
+++ b/homeassistant/components/hvv_departures/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"no_results": "\u041d\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0435\u0439 / \u0430\u0434\u0440\u0435\u0441\u043e\u043c."
},
diff --git a/homeassistant/components/hvv_departures/translations/zh-Hant.json b/homeassistant/components/hvv_departures/translations/zh-Hant.json
index ee22830c030400..5859493eeab6e8 100644
--- a/homeassistant/components/hvv_departures/translations/zh-Hant.json
+++ b/homeassistant/components/hvv_departures/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"no_results": "\u6c92\u6709\u7d50\u679c\u3002\u8acb\u5617\u8a66\u5176\u4ed6\u8eca\u7ad9/\u5730\u5740"
},
diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py
index d1baec315bf326..db34a21dadab22 100644
--- a/homeassistant/components/hyperion/light.py
+++ b/homeassistant/components/hyperion/light.py
@@ -1,8 +1,7 @@
-"""Support for Hyperion remotes."""
-import json
+"""Support for Hyperion-NG remotes."""
import logging
-import socket
+from hyperion import client, const
import voluptuous as vol
from homeassistant.components.light import (
@@ -16,6 +15,7 @@
LightEntity,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@@ -26,103 +26,91 @@
CONF_HDMI_PRIORITY = "hdmi_priority"
CONF_EFFECT_LIST = "effect_list"
+# As we want to preserve brightness control for effects (e.g. to reduce the
+# brightness for V4L), we need to persist the effect that is in flight, so
+# subsequent calls to turn_on will know the keep the effect enabled.
+# Unfortunately the Home Assistant UI does not easily expose a way to remove a
+# selected effect (there is no 'No Effect' option by default). Instead, we
+# create a new fake effect ("Solid") that is always selected by default for
+# showing a solid color. This is the same method used by WLED.
+KEY_EFFECT_SOLID = "Solid"
+
DEFAULT_COLOR = [255, 255, 255]
+DEFAULT_BRIGHTNESS = 255
+DEFAULT_EFFECT = KEY_EFFECT_SOLID
DEFAULT_NAME = "Hyperion"
+DEFAULT_ORIGIN = "Home Assistant"
DEFAULT_PORT = 19444
DEFAULT_PRIORITY = 128
DEFAULT_HDMI_PRIORITY = 880
-DEFAULT_EFFECT_LIST = [
- "HDMI",
- "Cinema brighten lights",
- "Cinema dim lights",
- "Knight rider",
- "Blue mood blobs",
- "Cold mood blobs",
- "Full color mood blobs",
- "Green mood blobs",
- "Red mood blobs",
- "Warm mood blobs",
- "Police Lights Single",
- "Police Lights Solid",
- "Rainbow mood",
- "Rainbow swirl fast",
- "Rainbow swirl",
- "Random",
- "Running dots",
- "System Shutdown",
- "Snake",
- "Sparks Color",
- "Sparks",
- "Strobe blue",
- "Strobe Raspbmc",
- "Strobe white",
- "Color traces",
- "UDP multicast listener",
- "UDP listener",
- "X-Mas",
-]
+DEFAULT_EFFECT_LIST = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
- list,
- vol.Length(min=3, max=3),
- [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
- ),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
- vol.Optional(
- CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
- ): cv.positive_int,
- vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
- cv.ensure_list, [cv.string]
- ),
- }
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"),
+ cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"),
+ cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
+ list,
+ vol.Length(min=3, max=3),
+ [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
+ ),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
+ vol.Optional(
+ CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
+ ): cv.positive_int,
+ vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ }
+ ),
)
+ICON_LIGHTBULB = "mdi:lightbulb"
+ICON_EFFECT = "mdi:lava-lamp"
+ICON_EXTERNAL_SOURCE = "mdi:video-input-hdmi"
+
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Hyperion server remote."""
name = config[CONF_NAME]
host = config[CONF_HOST]
port = config[CONF_PORT]
priority = config[CONF_PRIORITY]
- hdmi_priority = config[CONF_HDMI_PRIORITY]
- default_color = config[CONF_DEFAULT_COLOR]
- effect_list = config[CONF_EFFECT_LIST]
- device = Hyperion(
- name, host, port, priority, default_color, hdmi_priority, effect_list
- )
+ hyperion_client = client.HyperionClient(host, port)
+
+ if not await hyperion_client.async_client_connect():
+ raise PlatformNotReady
- if device.setup():
- add_entities([device])
+ async_add_entities([Hyperion(name, priority, hyperion_client)])
class Hyperion(LightEntity):
"""Representation of a Hyperion remote."""
- def __init__(
- self, name, host, port, priority, default_color, hdmi_priority, effect_list
- ):
+ def __init__(self, name, priority, hyperion_client):
"""Initialize the light."""
- self._host = host
- self._port = port
self._name = name
self._priority = priority
- self._hdmi_priority = hdmi_priority
- self._default_color = default_color
- self._rgb_color = [0, 0, 0]
- self._rgb_mem = [0, 0, 0]
- self._brightness = 255
- self._icon = "mdi:lightbulb"
- self._effect_list = effect_list
- self._effect = None
- self._skip_update = False
+ self._client = hyperion_client
+
+ # Active state representing the Hyperion instance.
+ self._set_internal_state(
+ brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID
+ )
+ self._effect_list = []
+
+ @property
+ def should_poll(self):
+ """Return whether or not this entity should be polled."""
+ return False
@property
def name(self):
@@ -142,7 +130,7 @@ def hs_color(self):
@property
def is_on(self):
"""Return true if not black."""
- return self._rgb_color != [0, 0, 0]
+ return self._client.is_on()
@property
def icon(self):
@@ -157,158 +145,233 @@ def effect(self):
@property
def effect_list(self):
"""Return the list of supported effects."""
- return self._effect_list
+ return (
+ self._effect_list
+ + const.KEY_COMPONENTID_EXTERNAL_SOURCES
+ + [KEY_EFFECT_SOLID]
+ )
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_HYPERION
- def turn_on(self, **kwargs):
+ @property
+ def available(self):
+ """Return server availability."""
+ return self._client.has_loaded_state
+
+ @property
+ def unique_id(self):
+ """Return a unique id for this instance."""
+ return self._client.id
+
+ async def async_turn_on(self, **kwargs):
"""Turn the lights on."""
+ # == Turn device on ==
+ # Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be
+ # preferable to enable LEDDEVICE after the settings (e.g. brightness,
+ # color, effect), but this is not possible due to:
+ # https://github.com/hyperion-project/hyperion.ng/issues/967
+ if not self.is_on:
+ if not await self._client.async_send_set_component(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL,
+ const.KEY_STATE: True,
+ }
+ }
+ ):
+ return
+
+ if not await self._client.async_send_set_component(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
+ const.KEY_STATE: True,
+ }
+ }
+ ):
+ return
+
+ # == Get key parameters ==
+ brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
+ effect = kwargs.get(ATTR_EFFECT, self._effect)
if ATTR_HS_COLOR in kwargs:
rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
- elif self._rgb_mem == [0, 0, 0]:
- rgb_color = self._default_color
else:
- rgb_color = self._rgb_mem
+ rgb_color = self._rgb_color
+
+ # == Set brightness ==
+ if self._brightness != brightness:
+ if not await self._client.async_send_set_adjustment(
+ **{
+ const.KEY_ADJUSTMENT: {
+ const.KEY_BRIGHTNESS: int(
+ round((float(brightness) * 100) / 255)
+ )
+ }
+ }
+ ):
+ return
- brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
+ # == Set an external source
+ if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
- if ATTR_EFFECT in kwargs:
- self._skip_update = True
- self._effect = kwargs[ATTR_EFFECT]
- if self._effect == "HDMI":
- self.json_request({"command": "clearall"})
- self._icon = "mdi:video-input-hdmi"
- self._brightness = 255
- self._rgb_color = [125, 125, 125]
- else:
- self.json_request(
- {
- "command": "effect",
- "priority": self._priority,
- "effect": {"name": self._effect},
+ # Clear any color/effect.
+ if not await self._client.async_send_clear(
+ **{const.KEY_PRIORITY: self._priority}
+ ):
+ return
+
+ # Turn off all external sources, except the intended.
+ for key in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
+ if not await self._client.async_send_set_component(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: key,
+ const.KEY_STATE: effect == key,
+ }
}
- )
- self._icon = "mdi:lava-lamp"
- self._rgb_color = [175, 0, 255]
- return
+ ):
+ return
- cal_color = [int(round(x * float(brightness) / 255)) for x in rgb_color]
- self.json_request(
- {"command": "color", "priority": self._priority, "color": cal_color}
- )
+ # == Set an effect
+ elif effect and effect != KEY_EFFECT_SOLID:
+ # This call should not be necessary, but without it there is no priorities-update issued:
+ # https://github.com/hyperion-project/hyperion.ng/issues/992
+ if not await self._client.async_send_clear(
+ **{const.KEY_PRIORITY: self._priority}
+ ):
+ return
- def turn_off(self, **kwargs):
- """Disconnect all remotes."""
- self.json_request({"command": "clearall"})
- self.json_request(
- {"command": "color", "priority": self._priority, "color": [0, 0, 0]}
- )
+ if not await self._client.async_send_set_effect(
+ **{
+ const.KEY_PRIORITY: self._priority,
+ const.KEY_EFFECT: {const.KEY_NAME: effect},
+ const.KEY_ORIGIN: DEFAULT_ORIGIN,
+ }
+ ):
+ return
+ # == Set a color
+ else:
+ if not await self._client.async_send_set_color(
+ **{
+ const.KEY_PRIORITY: self._priority,
+ const.KEY_COLOR: rgb_color,
+ const.KEY_ORIGIN: DEFAULT_ORIGIN,
+ }
+ ):
+ return
- def update(self):
- """Get the lights status."""
- # postpone the immediate state check for changes that take time
- if self._skip_update:
- self._skip_update = False
+ async def async_turn_off(self, **kwargs):
+ """Disable the LED output component."""
+ if not await self._client.async_send_set_component(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
+ const.KEY_STATE: False,
+ }
+ }
+ ):
return
- response = self.json_request({"command": "serverinfo"})
- if response:
- # workaround for outdated Hyperion
- if "activeLedColor" not in response["info"]:
- self._rgb_color = self._default_color
- self._rgb_mem = self._default_color
- self._brightness = 255
- self._icon = "mdi:lightbulb"
- self._effect = None
- return
- # Check if Hyperion is in ambilight mode trough an HDMI grabber
- try:
- active_priority = response["info"]["priorities"][0]["priority"]
- if active_priority == self._hdmi_priority:
- self._brightness = 255
- self._rgb_color = [125, 125, 125]
- self._icon = "mdi:video-input-hdmi"
- self._effect = "HDMI"
- return
- except (KeyError, IndexError):
- pass
-
- led_color = response["info"]["activeLedColor"]
- if not led_color or led_color[0]["RGB Value"] == [0, 0, 0]:
- # Get the active effect
- if response["info"].get("activeEffects"):
- self._rgb_color = [175, 0, 255]
- self._icon = "mdi:lava-lamp"
- try:
- s_name = response["info"]["activeEffects"][0]["script"]
- s_name = s_name.split("/")[-1][:-3].split("-")[0]
- self._effect = [
- x for x in self._effect_list if s_name.lower() in x.lower()
- ][0]
- except (KeyError, IndexError):
- self._effect = None
- # Bulb off state
- else:
- self._rgb_color = [0, 0, 0]
- self._icon = "mdi:lightbulb"
- self._effect = None
- else:
- # Get the RGB color
- self._rgb_color = led_color[0]["RGB Value"]
- self._brightness = max(self._rgb_color)
- self._rgb_mem = [
- int(round(float(x) * 255 / self._brightness))
- for x in self._rgb_color
- ]
- self._icon = "mdi:lightbulb"
- self._effect = None
-
- def setup(self):
- """Get the hostname of the remote."""
- response = self.json_request({"command": "serverinfo"})
- if response:
- if self._name == self._host:
- self._name = response["info"]["hostname"]
- return True
- return False
- def json_request(self, request, wait_for_response=False):
- """Communicate with the JSON server."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(5)
-
- try:
- sock.connect((self._host, self._port))
- except OSError:
- sock.close()
- return False
-
- sock.send(bytearray(f"{json.dumps(request)}\n", "utf-8"))
- try:
- buf = sock.recv(4096)
- except socket.timeout:
- # Something is wrong, assume it's offline
- sock.close()
- return False
-
- # Read until a newline or timeout
- buffering = True
- while buffering:
- if "\n" in str(buf, "utf-8"):
- response = str(buf, "utf-8").split("\n")[0]
- buffering = False
+ def _set_internal_state(self, brightness=None, rgb_color=None, effect=None):
+ """Set the internal state."""
+ if brightness is not None:
+ self._brightness = brightness
+ if rgb_color is not None:
+ self._rgb_color = rgb_color
+ if effect is not None:
+ self._effect = effect
+ if effect == KEY_EFFECT_SOLID:
+ self._icon = ICON_LIGHTBULB
+ elif effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
+ self._icon = ICON_EXTERNAL_SOURCE
else:
- try:
- more = sock.recv(4096)
- except socket.timeout:
- more = None
- if not more:
- buffering = False
- response = str(buf, "utf-8")
- else:
- buf += more
-
- sock.close()
- return json.loads(response)
+ self._icon = ICON_EFFECT
+
+ def _update_components(self, _=None):
+ """Update Hyperion components."""
+ self.async_write_ha_state()
+
+ def _update_adjustment(self, _=None):
+ """Update Hyperion adjustments."""
+ if self._client.adjustment:
+ brightness_pct = self._client.adjustment[0].get(
+ const.KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS
+ )
+ if brightness_pct < 0 or brightness_pct > 100:
+ return
+ self._set_internal_state(
+ brightness=int(round((brightness_pct * 255) / float(100)))
+ )
+ self.async_write_ha_state()
+
+ def _update_priorities(self, _=None):
+ """Update Hyperion priorities."""
+ visible_priority = self._client.visible_priority
+ if visible_priority:
+ componentid = visible_priority.get(const.KEY_COMPONENTID)
+ if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
+ self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid)
+ elif componentid == const.KEY_COMPONENTID_EFFECT:
+ # Owner is the effect name.
+ # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
+ self._set_internal_state(
+ rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER]
+ )
+ elif componentid == const.KEY_COMPONENTID_COLOR:
+ self._set_internal_state(
+ rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB],
+ effect=KEY_EFFECT_SOLID,
+ )
+ self.async_write_ha_state()
+
+ def _update_effect_list(self, _=None):
+ """Update Hyperion effects."""
+ if not self._client.effects:
+ return
+ effect_list = []
+ for effect in self._client.effects or []:
+ if const.KEY_NAME in effect:
+ effect_list.append(effect[const.KEY_NAME])
+ if effect_list:
+ self._effect_list = effect_list
+ self.async_write_ha_state()
+
+ def _update_full_state(self):
+ """Update full Hyperion state."""
+ self._update_adjustment()
+ self._update_priorities()
+ self._update_effect_list()
+
+ _LOGGER.debug(
+ "Hyperion full state update: On=%s,Brightness=%i,Effect=%s "
+ "(%i effects total),Color=%s",
+ self.is_on,
+ self._brightness,
+ self._effect,
+ len(self._effect_list),
+ self._rgb_color,
+ )
+
+ def _update_client(self, json):
+ """Update client connection state."""
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self):
+ """Register callbacks when entity added to hass."""
+ self._client.set_callbacks(
+ {
+ f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment,
+ f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components,
+ f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list,
+ f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities,
+ f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client,
+ }
+ )
+
+ # Load initial state.
+ self._update_full_state()
+ return True
diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json
index 6d9d0ae4d9def4..4a9bf2ada8c930 100644
--- a/homeassistant/components/hyperion/manifest.json
+++ b/homeassistant/components/hyperion/manifest.json
@@ -2,5 +2,6 @@
"domain": "hyperion",
"name": "Hyperion",
"documentation": "https://www.home-assistant.io/integrations/hyperion",
- "codeowners": []
+ "requirements": ["hyperion-py==0.3.0"],
+ "codeowners": ["@dermotduffy"]
}
diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py
index d64ec711198c68..c083aee7c1c076 100644
--- a/homeassistant/components/iaqualink/config_flow.py
+++ b/homeassistant/components/iaqualink/config_flow.py
@@ -23,7 +23,7 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None):
# Supporting a single account.
entries = self.hass.config_entries.async_entries(DOMAIN)
if entries:
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
errors = {}
@@ -36,7 +36,7 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None):
await aqualink.login()
return self.async_create_entry(title=username, data=user_input)
except AqualinkLoginException:
- errors["base"] = "connection_failure"
+ errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="user",
diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json
index a861fd35420855..5e7fcf6aa7a743 100644
--- a/homeassistant/components/iaqualink/strings.json
+++ b/homeassistant/components/iaqualink/strings.json
@@ -11,10 +11,10 @@
}
},
"error": {
- "connection_failure": "Unable to connect to iAqualink. Check your username and password."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_setup": "You can only configure a single iAqualink connection."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/iaqualink/translations/ca.json b/homeassistant/components/iaqualink/translations/ca.json
index 196f4970f93133..f0ca2ef59e0f96 100644
--- a/homeassistant/components/iaqualink/translations/ca.json
+++ b/homeassistant/components/iaqualink/translations/ca.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 d'iAqualink."
+ "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 d'iAqualink.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_failure": "No s'ha pogut connectar amb iAqualink. Comprova el nom d'usuari i la contrasenya."
},
"step": {
diff --git a/homeassistant/components/iaqualink/translations/en.json b/homeassistant/components/iaqualink/translations/en.json
index 8c66ad88e6b770..3ddc3a139245fe 100644
--- a/homeassistant/components/iaqualink/translations/en.json
+++ b/homeassistant/components/iaqualink/translations/en.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "You can only configure a single iAqualink connection."
+ "already_setup": "You can only configure a single iAqualink connection.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_failure": "Unable to connect to iAqualink. Check your username and password."
},
"step": {
diff --git a/homeassistant/components/iaqualink/translations/it.json b/homeassistant/components/iaqualink/translations/it.json
index 568f91961f2f3d..f1abb66006f7cf 100644
--- a/homeassistant/components/iaqualink/translations/it.json
+++ b/homeassistant/components/iaqualink/translations/it.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "\u00c8 possibile configurare una sola connessione iAqualink."
+ "already_setup": "\u00c8 possibile configurare una sola connessione iAqualink.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_failure": "Impossibile connettersi a iAqualink. Controllare il nome utente e la password."
},
"step": {
diff --git a/homeassistant/components/iaqualink/translations/pl.json b/homeassistant/components/iaqualink/translations/pl.json
index f0582b633f1ea8..ae13f8dfcfdba0 100644
--- a/homeassistant/components/iaqualink/translations/pl.json
+++ b/homeassistant/components/iaqualink/translations/pl.json
@@ -4,6 +4,7 @@
"already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno po\u0142\u0105czenie iAqualink."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_failure": "Nie mo\u017cna po\u0142\u0105czy\u0107 z iAqualink. Sprawd\u017a nazw\u0119 u\u017cytkownika i has\u0142o."
},
"step": {
diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py
index 052e5b98379cb5..a26226a9e182bd 100644
--- a/homeassistant/components/icloud/config_flow.py
+++ b/homeassistant/components/icloud/config_flow.py
@@ -113,7 +113,7 @@ async def async_step_user(self, user_input=None):
except PyiCloudFailedLoginException as error:
_LOGGER.error("Error logging into iCloud service: %s", error)
self.api = None
- errors[CONF_USERNAME] = "login"
+ errors[CONF_USERNAME] = "invalid_auth"
return await self._show_setup_form(user_input, errors)
if self.api.requires_2sa:
diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json
index 7153bb9340e80b..43bc204f451e55 100644
--- a/homeassistant/components/icloud/strings.json
+++ b/homeassistant/components/icloud/strings.json
@@ -26,13 +26,13 @@
}
},
"error": {
- "login": "Login error: please check your email & password",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"send_verification_code": "Failed to send verification code",
"validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again"
},
"abort": {
- "already_configured": "Account already configured",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"no_device": "None of your devices have \"Find my iPhone\" activated"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json
index 5fa4ee6626df3e..9b3d0ea5ce46f6 100644
--- a/homeassistant/components/icloud/translations/ca.json
+++ b/homeassistant/components/icloud/translations/ca.json
@@ -5,6 +5,7 @@
"no_device": "Cap dels teus dispositius t\u00e9 activada la opci\u00f3 \"Troba el meu iPhone\""
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"login": "Error d'inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya",
"send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3",
"validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s"
diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json
index 0e94418822284a..b76f98b21dadb3 100644
--- a/homeassistant/components/icloud/translations/en.json
+++ b/homeassistant/components/icloud/translations/en.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Account already configured",
+ "already_configured": "Account is already configured",
"no_device": "None of your devices have \"Find my iPhone\" activated"
},
"error": {
+ "invalid_auth": "Invalid authentication",
"login": "Login error: please check your email & password",
"send_verification_code": "Failed to send verification code",
"validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again"
diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json
index 49bd9d612eb6ba..9322716f73d6af 100644
--- a/homeassistant/components/icloud/translations/es.json
+++ b/homeassistant/components/icloud/translations/es.json
@@ -5,6 +5,7 @@
"no_device": "Ninguno de tus dispositivos tiene activado \"Buscar mi iPhone\""
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"login": "Error de inicio de sesi\u00f3n: comprueba tu direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a",
"send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n",
"validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo"
diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json
new file mode 100644
index 00000000000000..2227b7442a79c6
--- /dev/null
+++ b/homeassistant/components/icloud/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json
index 0ede270fd79856..000696d5b7d2f2 100644
--- a/homeassistant/components/icloud/translations/fr.json
+++ b/homeassistant/components/icloud/translations/fr.json
@@ -5,6 +5,7 @@
"no_device": "Aucun de vos appareils n'a activ\u00e9 \"Find my iPhone\""
},
"error": {
+ "invalid_auth": "Authentification invalide",
"login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe",
"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/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json
index 27429081e1e765..0d91335c41a5c1 100644
--- a/homeassistant/components/icloud/translations/it.json
+++ b/homeassistant/components/icloud/translations/it.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Account gi\u00e0 configurato",
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
"no_device": "Nessuno dei tuoi dispositivi ha attivato \"Trova il mio iPhone\""
},
"error": {
+ "invalid_auth": "Autenticazione non valida",
"login": "Errore di accesso: si prega di controllare la tua e-mail e la password",
"send_verification_code": "Impossibile inviare il codice di verifica",
"validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica"
diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json
index 9805c769a67680..0068468d44f42e 100644
--- a/homeassistant/components/icloud/translations/no.json
+++ b/homeassistant/components/icloud/translations/no.json
@@ -5,6 +5,7 @@
"no_device": "Ingen av enhetene dine har \"Finn min iPhone\" aktivert"
},
"error": {
+ "invalid_auth": "Ugyldig godkjenning",
"login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt",
"send_verification_code": "Kunne ikke sende bekreftelseskode",
"validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt"
diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json
index 9066961ce29577..55725302617ff1 100644
--- a/homeassistant/components/icloud/translations/ru.json
+++ b/homeassistant/components/icloud/translations/ru.json
@@ -5,6 +5,7 @@
"no_device": "\u041d\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0438\u0437 \u0412\u0430\u0448\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \"\u041d\u0430\u0439\u0442\u0438 iPhone\"."
},
"error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
"send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.",
"validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430."
diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json
index b637e0de13da8b..9002cf307563c3 100644
--- a/homeassistant/components/ifttt/strings.json
+++ b/homeassistant/components/ifttt/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages."
},
"create_entry": {
diff --git a/homeassistant/components/ifttt/translations/ca.json b/homeassistant/components/ifttt/translations/ca.json
index 29f9fa29fe99dc..8706b5655dc98a 100644
--- a/homeassistant/components/ifttt/translations/ca.json
+++ b/homeassistant/components/ifttt/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de IFTTT.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
diff --git a/homeassistant/components/ifttt/translations/el.json b/homeassistant/components/ifttt/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/en.json b/homeassistant/components/ifttt/translations/en.json
index 5a54f5a9aa5eaf..006680f940262a 100644
--- a/homeassistant/components/ifttt/translations/en.json
+++ b/homeassistant/components/ifttt/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json
index 713cff93847bb1..7af87b11f35d64 100644
--- a/homeassistant/components/ifttt/translations/es.json
+++ b/homeassistant/components/ifttt/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes IFTTT.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant debes usar la acci\u00f3n \"Make a web request\" del [applet IFTTT Webhook]({applet_url}).\n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n- Tipo de contenido: application/json\n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes."
diff --git a/homeassistant/components/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json
new file mode 100644
index 00000000000000..c378c953b499b7
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/et.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "create_entry": {
+ "default": "S\u00fcndmuste saatmiseks Home Assistantile peate kasutama toimingut \"Make a web request\" [IFTTT Webhooki apletilt] ({applet_url}).\n\nSisestage j\u00e4rgmine teave:\n\n- URL: {webhook_url}.\n- Method: POST\n- Content Type: application/json\n\nVaadake [dokumentatsiooni]({docs_url}) kuidas seadistada sissetulevate andmete t\u00f6\u00f6tlemiseks automatiseerimisi."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/fr.json b/homeassistant/components/ifttt/translations/fr.json
index 823801e9743463..b8782f1452e10d 100644
--- a/homeassistant/components/ifttt/translations/fr.json
+++ b/homeassistant/components/ifttt/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages IFTTT.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook]({applet_url}). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation]({docs_url}) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes."
diff --git a/homeassistant/components/ifttt/translations/it.json b/homeassistant/components/ifttt/translations/it.json
index 1989efd733ce4e..101e91e7f98448 100644
--- a/homeassistant/components/ifttt/translations/it.json
+++ b/homeassistant/components/ifttt/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi IFTTT.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, \u00e8 necessario utilizzare l'azione \"Crea una richiesta web\" dall'[applet IFTTT Webhook]({applet_url}). \n\n Compilare le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n - Tipo di contenuto: application/json \n\nVedere [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo."
diff --git a/homeassistant/components/ifttt/translations/lb.json b/homeassistant/components/ifttt/translations/lb.json
index a13dd946a3e5a7..7ff324b45a259f 100644
--- a/homeassistant/components/ifttt/translations/lb.json
+++ b/homeassistant/components/ifttt/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir IFTTT Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9ckemusst dir d'Aktioun \"Make a web request\" vum [IFTTT Webhook applet] ({applet_url}) benotzen.\n\nGitt folgend Informatiounen un:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nKuckt iech [Dokumentatioun]({docs_url}) w\u00e9i een Automatisatioune mat empfaangene Donn\u00e9e konfigur\u00e9iert."
diff --git a/homeassistant/components/ifttt/translations/no.json b/homeassistant/components/ifttt/translations/no.json
index 54c2f71f40bac7..bb1605c7499c68 100644
--- a/homeassistant/components/ifttt/translations/no.json
+++ b/homeassistant/components/ifttt/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data."
diff --git a/homeassistant/components/ifttt/translations/ru.json b/homeassistant/components/ifttt/translations/ru.json
index c78fd1090ad026..4f3efe9e267f9a 100644
--- a/homeassistant/components/ifttt/translations/ru.json
+++ b/homeassistant/components/ifttt/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 IFTTT.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445."
diff --git a/homeassistant/components/ifttt/translations/zh-Hant.json b/homeassistant/components/ifttt/translations/zh-Hant.json
index 8337aa82479d4b..cb57097e4f207b 100644
--- a/homeassistant/components/ifttt/translations/zh-Hant.json
+++ b/homeassistant/components/ifttt/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8981\u7531 [IFTTT Webhook applet]({applet_url}) \u547c\u53eb\u300c\u9032\u884c Web \u8acb\u6c42\u300d\u52d5\u4f5c\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002"
diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json
index 4fc9c2d1d05ea0..246ea3871400ec 100644
--- a/homeassistant/components/image/manifest.json
+++ b/homeassistant/components/image/manifest.json
@@ -4,9 +4,7 @@
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
"requirements": ["pillow==7.2.0"],
- "ssdp": [],
- "zeroconf": [],
- "homekit": {},
"dependencies": ["http"],
- "codeowners": ["@home-assistant/core"]
+ "codeowners": ["@home-assistant/core"],
+ "quality_scale": "internal"
}
diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py
index 84ba5b45fc4d17..4617e78a6ece4c 100644
--- a/homeassistant/components/image_processing/__init__.py
+++ b/homeassistant/components/image_processing/__init__.py
@@ -173,9 +173,7 @@ def device_class(self):
@property
def state_attributes(self):
"""Return device specific state attributes."""
- attr = {ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces}
-
- return attr
+ return {ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces}
def process_faces(self, faces, total):
"""Send event with detected faces and store data."""
diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py
index db49e119235697..45d3a4f5a25d0c 100644
--- a/homeassistant/components/influxdb/__init__.py
+++ b/homeassistant/components/influxdb/__init__.py
@@ -57,6 +57,7 @@
CONF_PASSWORD,
CONF_PATH,
CONF_PORT,
+ CONF_PRECISION,
CONF_RETRY_COUNT,
CONF_SSL,
CONF_TAGS,
@@ -307,13 +308,13 @@ def get_influx_connection(conf, test_write=False, test_read=False):
kwargs = {
CONF_TIMEOUT: TIMEOUT,
}
+ precision = conf.get(CONF_PRECISION)
if conf[CONF_API_VERSION] == API_VERSION_2:
kwargs[CONF_URL] = conf[CONF_URL]
kwargs[CONF_TOKEN] = conf[CONF_TOKEN]
kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG]
bucket = conf.get(CONF_BUCKET)
-
influx = InfluxDBClientV2(**kwargs)
query_api = influx.query_api()
initial_write_mode = SYNCHRONOUS if test_write else ASYNCHRONOUS
@@ -322,7 +323,7 @@ def get_influx_connection(conf, test_write=False, test_read=False):
def write_v2(json):
"""Write data to V2 influx."""
try:
- write_api.write(bucket=bucket, record=json)
+ write_api.write(bucket=bucket, record=json, write_precision=precision)
except (urllib3.exceptions.HTTPError, OSError) as exc:
raise ConnectionError(CONNECTION_ERROR % exc) from exc
except ApiException as exc:
@@ -393,7 +394,7 @@ def close_v2():
def write_v1(json):
"""Write data to V1 influx."""
try:
- influx.write_points(json)
+ influx.write_points(json, time_precision=precision)
except (
requests.exceptions.RequestException,
exceptions.InfluxDBServerError,
diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py
index c1b5ce3a59144a..029e4d482e8095 100644
--- a/homeassistant/components/influxdb/const.py
+++ b/homeassistant/components/influxdb/const.py
@@ -29,6 +29,7 @@
CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain"
CONF_RETRY_COUNT = "max_retries"
CONF_IGNORE_ATTRIBUTES = "ignore_attributes"
+CONF_PRECISION = "precision"
CONF_LANGUAGE = "language"
CONF_QUERIES = "queries"
@@ -136,6 +137,7 @@
vol.Optional(CONF_PATH): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL): cv.boolean,
+ vol.Optional(CONF_PRECISION): vol.In(["ms", "s", "us", "ns"]),
# Connection config for V1 API only.
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py
index eb5b0ce609101f..2fcf4baaba8f13 100644
--- a/homeassistant/components/influxdb/sensor.py
+++ b/homeassistant/components/influxdb/sensor.py
@@ -224,11 +224,6 @@ def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
def update(self):
"""Get the latest data from Influxdb and updates the states."""
self.data.update()
diff --git a/homeassistant/components/input_boolean/translations/et.json b/homeassistant/components/input_boolean/translations/et.json
index 3edfbf3cb5da5a..e0b2d46b168e00 100644
--- a/homeassistant/components/input_boolean/translations/et.json
+++ b/homeassistant/components/input_boolean/translations/et.json
@@ -5,5 +5,5 @@
"on": "Sees"
}
},
- "title": "Sisesta t\u00f5ev\u00e4\u00e4rtus"
+ "title": "T\u00f5ev\u00e4\u00e4rtuse abiline"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_boolean/translations/no.json b/homeassistant/components/input_boolean/translations/no.json
index b0a608a1754293..f08c1e111deee9 100644
--- a/homeassistant/components/input_boolean/translations/no.json
+++ b/homeassistant/components/input_boolean/translations/no.json
@@ -5,5 +5,5 @@
"on": "P\u00e5"
}
},
- "title": "Inndata boolsk"
+ "title": "Innputt boolsk"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_datetime/translations/et.json b/homeassistant/components/input_datetime/translations/et.json
index e72e7b102880b7..83acbf892621ce 100644
--- a/homeassistant/components/input_datetime/translations/et.json
+++ b/homeassistant/components/input_datetime/translations/et.json
@@ -1,3 +1,3 @@
{
- "title": "Sisesta kuup\u00e4ev ja kellaaeg"
+ "title": "Kuup\u00e4eva ja kellaaja abiline"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_datetime/translations/no.json b/homeassistant/components/input_datetime/translations/no.json
index e9a36c0fc88d18..716ca6fbbc01e6 100644
--- a/homeassistant/components/input_datetime/translations/no.json
+++ b/homeassistant/components/input_datetime/translations/no.json
@@ -1,3 +1,3 @@
{
- "title": "Inndata datotid"
+ "title": "Innputt datotid"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_number/translations/et.json b/homeassistant/components/input_number/translations/et.json
index f4182fbb3d5b9d..a241a89ff5362b 100644
--- a/homeassistant/components/input_number/translations/et.json
+++ b/homeassistant/components/input_number/translations/et.json
@@ -1,3 +1,3 @@
{
- "title": "Sisendi number"
+ "title": "Arvv\u00e4\u00e4rtuse abiline"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_number/translations/no.json b/homeassistant/components/input_number/translations/no.json
index cc918fabb2f99b..3988fe3eace615 100644
--- a/homeassistant/components/input_number/translations/no.json
+++ b/homeassistant/components/input_number/translations/no.json
@@ -1,3 +1,3 @@
{
- "title": "Inndata nummer"
+ "title": "Innputt nummer"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_select/translations/et.json b/homeassistant/components/input_select/translations/et.json
index 22378cd35a0dd0..5cce5f5ce0b402 100644
--- a/homeassistant/components/input_select/translations/et.json
+++ b/homeassistant/components/input_select/translations/et.json
@@ -1,3 +1,3 @@
{
- "title": "Vali sisend"
+ "title": "Valikmen\u00fc\u00fc abiline"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_select/translations/no.json b/homeassistant/components/input_select/translations/no.json
index c5802730c428d3..a87771349a8e1a 100644
--- a/homeassistant/components/input_select/translations/no.json
+++ b/homeassistant/components/input_select/translations/no.json
@@ -1,3 +1,3 @@
{
- "title": "Inndata valg"
+ "title": "Innputt valg"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_text/translations/et.json b/homeassistant/components/input_text/translations/et.json
index 047874d6328446..42d7d57f419cb3 100644
--- a/homeassistant/components/input_text/translations/et.json
+++ b/homeassistant/components/input_text/translations/et.json
@@ -1,3 +1,3 @@
{
- "title": "Teksti sisestamine"
+ "title": "Tekstisisestuse abiline"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_text/translations/no.json b/homeassistant/components/input_text/translations/no.json
index bf41f9dc43cc72..9c1141de543fac 100644
--- a/homeassistant/components/input_text/translations/no.json
+++ b/homeassistant/components/input_text/translations/no.json
@@ -1,3 +1,3 @@
{
- "title": "Inndata tekst"
+ "title": "Innputt tekst"
}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json
index 871629b68775c9..d20f56054b3a59 100644
--- a/homeassistant/components/insteon/manifest.json
+++ b/homeassistant/components/insteon/manifest.json
@@ -2,7 +2,7 @@
"domain": "insteon",
"name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon",
- "requirements": ["pyinsteon==1.0.7"],
+ "requirements": ["pyinsteon==1.0.8"],
"codeowners": ["@teharris1"],
"config_flow": true
}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json
new file mode 100644
index 00000000000000..dfefff8c559eba
--- /dev/null
+++ b/homeassistant/components/insteon/translations/de.json
@@ -0,0 +1,66 @@
+{
+ "config": {
+ "step": {
+ "hub2": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ },
+ "hubv1": {
+ "data": {
+ "host": "IP-Adresse",
+ "port": "Port"
+ }
+ },
+ "hubv2": {
+ "data": {
+ "host": "IP-Adresse",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ }
+ },
+ "init": {
+ "title": "Insteon"
+ },
+ "plm": {
+ "title": "Insteon PLM"
+ },
+ "user": {
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "add_override": {
+ "title": "Insteon"
+ },
+ "add_x10": {
+ "data": {
+ "platform": "Plattform"
+ },
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "IP-Adresse",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ },
+ "title": "Insteon"
+ },
+ "init": {
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json
new file mode 100644
index 00000000000000..f35d5bad3434fd
--- /dev/null
+++ b/homeassistant/components/insteon/translations/el.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "step": {
+ "hubv1": {
+ "data": {
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Insteon Hub Version 1 (\u03c0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf 2014).",
+ "title": "Insteon Hub Version 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Insteon Hub Version 2.",
+ "title": "Insteon Hub Version 2"
+ },
+ "user": {
+ "data": {
+ "modem_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc."
+ },
+ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc Insteon.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "remove_override": {
+ "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7"
+ },
+ "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 X10",
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json
index 45b85201a3c7c2..f18df0110489ce 100644
--- a/homeassistant/components/insteon/translations/fr.json
+++ b/homeassistant/components/insteon/translations/fr.json
@@ -48,14 +48,19 @@
},
"init": {
"data": {
- "hubv1": "Hub Version 1 (avant 2014)"
+ "hubv1": "Hub Version 1 (avant 2014)",
+ "hubv2": "Hub version 2",
+ "plm": "Modem PowerLink (PLM)"
},
+ "description": "S\u00e9lectionnez le type de modem Insteon.",
"title": "Insteon"
},
"plm": {
"data": {
"device": "Chemin du p\u00e9riph\u00e9rique USB"
- }
+ },
+ "description": "Configurez le modem Insteon PowerLink (PLM).",
+ "title": "Insteon PLM"
},
"user": {
"data": {
@@ -68,21 +73,27 @@
},
"options": {
"abort": {
+ "already_configured": "Une connexion Insteon par modem est d\u00e9j\u00e0 configur\u00e9e",
"cannot_connect": "Impossible de se connecter au modem Insteon"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
+ "input_error": "Entr\u00e9es non valides, veuillez v\u00e9rifier vos valeurs.",
"select_single": "S\u00e9lectionnez une option"
},
"step": {
"add_override": {
"data": {
- "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)"
+ "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)"
},
+ "description": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.",
"title": "Insteon"
},
"add_x10": {
"data": {
+ "housecode": "Code maison (a - p)",
"platform": "Plate-forme",
"steps": "Pas de gradateur (pour les appareils d'\u00e9clairage uniquement, par d\u00e9faut 22)",
"unitcode": "Code de l'unit\u00e9 (1-16)"
@@ -97,6 +108,7 @@
"port": "Port",
"username": "Nom d'utilisateur"
},
+ "description": "Modifiez les informations de connexion Insteon Hub. Vous devez red\u00e9marrer Home Assistant apr\u00e8s avoir effectu\u00e9 cette modification. Cela ne change pas la configuration du Hub lui-m\u00eame. Pour modifier la configuration dans le Hub, utilisez l'application Hub.",
"title": "Insteon"
},
"init": {
diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json
new file mode 100644
index 00000000000000..7c77bd49e27f38
--- /dev/null
+++ b/homeassistant/components/insteon/translations/ko.json
@@ -0,0 +1,111 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Insteon \ubaa8\ub380 \uc5f0\uacb0\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ },
+ "step": {
+ "hub1": {
+ "data": {
+ "host": "\ud5c8\ube0c IP \uc8fc\uc18c",
+ "port": "IP \ud3ec\ud2b8"
+ },
+ "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Insteon Hub \ubc84\uc804 1"
+ },
+ "hub2": {
+ "data": {
+ "host": "\ud5c8\ube0c IP \uc8fc\uc18c",
+ "port": "IP \ud3ec\ud2b8"
+ },
+ "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Insteon Hub \ubc84\uc804 2"
+ },
+ "hubv1": {
+ "data": {
+ "host": "IP \uc8fc\uc18c",
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Insteon Hub \ubc84\uc804 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "IP \uc8fc\uc18c",
+ "password": "\uc554\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ },
+ "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Insteon Hub \ubc84\uc804 2"
+ },
+ "init": {
+ "data": {
+ "hubv1": "\ud5c8\ube0c \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)"
+ }
+ },
+ "plm": {
+ "data": {
+ "device": "USB \uc7a5\uce58 \uacbd\ub85c"
+ }
+ },
+ "user": {
+ "data": {
+ "modem_type": "\ubaa8\ub380 \uc720\ud615."
+ },
+ "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Insteon \ubaa8\ub380\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "select_single": "\uc635\uc158 \uc120\ud0dd"
+ },
+ "step": {
+ "add_override": {
+ "data": {
+ "cat": "\uc7a5\uce58 \ubc94\uc8fc(\uc608: 0x10)"
+ }
+ },
+ "add_x10": {
+ "data": {
+ "steps": "\ub514\uba38 \ub2e8\uacc4(\ub77c\uc774\ud2b8 \uc7a5\uce58\uc5d0\ub9cc, \uae30\ubcf8\uac12 22)",
+ "unitcode": "\ub2e8\uc704 \ucf54\ub4dc (1-16)"
+ },
+ "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4."
+ },
+ "init": {
+ "data": {
+ "add_override": "\uc7a5\uce58 Override \ucd94\uac00",
+ "add_x10": "X10 \uc7a5\uce58\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.",
+ "change_hub_config": "\ud5c8\ube0c \uad6c\uc131\uc744 \ubcc0\uacbd\ud569\ub2c8\ub2e4.",
+ "remove_override": "\uc7a5\uce58 Override \uc81c\uac70",
+ "remove_x10": "X10 \uc7a5\uce58\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4."
+ },
+ "description": "\uad6c\uc131 \ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ },
+ "remove_override": {
+ "data": {
+ "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ },
+ "description": "\uc7a5\uce58 Override \uc81c\uac70"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ },
+ "description": "X10 \uc7a5\uce58 \uc81c\uac70"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/lb.json b/homeassistant/components/insteon/translations/lb.json
index bff6ff757c1bec..a0b327a655fc16 100644
--- a/homeassistant/components/insteon/translations/lb.json
+++ b/homeassistant/components/insteon/translations/lb.json
@@ -27,6 +27,24 @@
"description": "Insteon Hub Versioun 2 konfigur\u00e9ieren.",
"title": "Insteon Hub Versioun 2"
},
+ "hubv1": {
+ "data": {
+ "host": "IP Adresse",
+ "port": "Port"
+ },
+ "description": "Insteon Hub Versioun 1 (pre-2014) konfigur\u00e9ieren.",
+ "title": "Insteon Hub Versioun 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "IP Adresse",
+ "password": "Passwuert",
+ "port": "Port",
+ "username": "Benotzernumm"
+ },
+ "description": "Insteon Hub Versioun 2 konfigur\u00e9ieren.",
+ "title": "Insteon Hub Versioun 2"
+ },
"init": {
"data": {
"hubv1": "Hub Versioun 1 (Pre-2014)",
@@ -42,6 +60,13 @@
},
"description": "Insteon PowerLink Modem (PLM) konfigur\u00e9ieren.",
"title": "Insteon PLM"
+ },
+ "user": {
+ "data": {
+ "modem_type": "Typ vu Modem."
+ },
+ "description": "Insteon Modem Typ auswielen.",
+ "title": "Insteon"
}
}
},
diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json
new file mode 100644
index 00000000000000..538755dd013fe3
--- /dev/null
+++ b/homeassistant/components/insteon/translations/nl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "step": {
+ "hub2": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ },
+ "hubv2": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ },
+ "user": {
+ "data": {
+ "modem_type": "Modemtype."
+ },
+ "description": "Selecteer het Insteon-modemtype.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "change_hub_config": {
+ "data": {
+ "host": "IP-adres",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json
index cb697462ca9bc9..2b506fb05d0c1a 100644
--- a/homeassistant/components/insteon/translations/pl.json
+++ b/homeassistant/components/insteon/translations/pl.json
@@ -1,18 +1,40 @@
{
"config": {
"abort": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "already_configured": "Po\u0142\u0105czenie z modemem Insteon jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "select_single": "Wybierz jedn\u0105 z opcji."
},
"step": {
+ "hub1": {
+ "data": {
+ "host": "Adres IP Huba",
+ "port": "Numer portu IP"
+ },
+ "description": "Konfiguracja Huba Insteon \u2014 wersja 1 (sprzed 2014r.)",
+ "title": "Hub Insteon \u2014 wersja 1"
+ },
+ "hub2": {
+ "data": {
+ "host": "Adres IP Huba",
+ "password": "[%key_id:common::config_flow::data::password%]",
+ "port": "Numer portu IP",
+ "username": "[%key_id:common::config_flow::data::username%]"
+ },
+ "description": "Konfiguracja Huba Insteon \u2014 wersja 2.",
+ "title": "Hub Insteon \u2014 wersja 2"
+ },
"hubv1": {
"data": {
"host": "Adres IP",
"port": "Port"
- }
+ },
+ "description": "Konfiguracja Huba Insteon \u2014 wersja 1 (sprzed 2014r.)",
+ "title": "Hub Insteon \u2014 wersja 1"
},
"hubv2": {
"data": {
@@ -20,30 +42,99 @@
"password": "Has\u0142o",
"port": "Port",
"username": "Nazwa u\u017cytkownika"
- }
+ },
+ "description": "Konfiguracja Huba Insteon \u2014 wersja 2.",
+ "title": "Hub Insteon \u2014 wersja 2"
+ },
+ "init": {
+ "data": {
+ "hubv1": "Hub \u2014 wersja 1 (sprzed 2014r.)",
+ "hubv2": "Hub \u2014 wersja 2",
+ "plm": "Modem PowerLink (PLM)"
+ },
+ "description": "Wybierz typ modemu Insteon.",
+ "title": "Insteon"
},
"plm": {
"data": {
"device": "\u015acie\u017cka urz\u0105dzenia USB"
- }
+ },
+ "description": "Konfiguracja modemu Insteon PowerLink (PLM).",
+ "title": "Insteon PLM"
},
"user": {
+ "data": {
+ "modem_type": "Typ modemu."
+ },
+ "description": "Wybierz typ modemu Insteon.",
"title": "Insteon"
}
}
},
"options": {
+ "abort": {
+ "already_configured": "Po\u0142\u0105czenie z modemem Insteon jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z modemem Insteon"
+ },
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "input_error": "Nieprawid\u0142owe wpisy, sprawd\u017a wpisane warto\u015bci.",
+ "select_single": "Wybierz jedn\u0105 z opcji."
},
"step": {
+ "add_override": {
+ "data": {
+ "address": "Adres urz\u0105dzenia (np. 1a2b3c)",
+ "cat": "Kategoria urz\u0105dzenia (np. 0x10)",
+ "subcat": "Podkategoria urz\u0105dzenia (np. 0x0a)"
+ },
+ "description": "Dodawanie nadpisanie urz\u0105dzenia.",
+ "title": "Insteon"
+ },
+ "add_x10": {
+ "data": {
+ "housecode": "Housecode (a - p)",
+ "platform": "Platforma",
+ "steps": "Kroki \u015bciemniacza (tylko dla urz\u0105dze\u0144 o\u015bwietleniowych, domy\u015blnie 22)",
+ "unitcode": "Unitcode (1\u201316)"
+ },
+ "description": "Zmie\u0144 has\u0142o Huba Insteon.",
+ "title": "Insteon"
+ },
"change_hub_config": {
"data": {
"host": "Adres IP",
"password": "Has\u0142o",
"port": "Port",
"username": "Nazwa u\u017cytkownika"
- }
+ },
+ "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany, musisz ponownie uruchomi\u0107 Home Assistant. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.",
+ "title": "Insteon"
+ },
+ "init": {
+ "data": {
+ "add_override": "Dodawanie nadpisanie urz\u0105dzenia.",
+ "add_x10": "Dodaj urz\u0105dzenie X10.",
+ "change_hub_config": "Zmie\u0144 konfiguracj\u0119 Huba.",
+ "remove_override": "Usu\u0144 nadpisanie urz\u0105dzenia.",
+ "remove_x10": "Usu\u0144 urz\u0105dzenie X10."
+ },
+ "description": "Wybierz opcj\u0119 do skonfigurowania.",
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "data": {
+ "address": "Wybierz adres urz\u0105dzenia do usuni\u0119cia"
+ },
+ "description": "Usu\u0144 nadpisanie urz\u0105dzenia",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "Wybierz adres urz\u0105dzenia do usuni\u0119cia"
+ },
+ "description": "Usu\u0144 urz\u0105dzenie X10",
+ "title": "Insteon"
}
}
}
diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py
index 6169e084a02cf8..a776920b8e644c 100644
--- a/homeassistant/components/integration/sensor.py
+++ b/homeassistant/components/integration/sensor.py
@@ -203,8 +203,7 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- state_attr = {ATTR_SOURCE_ID: self._sensor_source_id}
- return state_attr
+ return {ATTR_SOURCE_ID: self._sensor_source_id}
@property
def icon(self):
diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py
index 62dd72973da081..f9c682bf52743c 100644
--- a/homeassistant/components/ios/notify.py
+++ b/homeassistant/components/ios/notify.py
@@ -12,6 +12,7 @@
ATTR_TITLE_DEFAULT,
BaseNotificationService,
)
+from homeassistant.const import HTTP_CREATED, HTTP_TOO_MANY_REQUESTS
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -90,13 +91,13 @@ def send_message(self, message="", **kwargs):
req = requests.post(PUSH_URL, json=data, timeout=10)
- if req.status_code != 201:
+ if req.status_code != HTTP_CREATED:
fallback_error = req.json().get("errorMessage", "Unknown error")
fallback_message = (
f"Internal server error, please try again later: {fallback_error}"
)
message = req.json().get("message", fallback_message)
- if req.status_code == 429:
+ if req.status_code == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(message)
log_rate_limits(self.hass, target, req.json(), 30)
else:
diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py
index 497e94a08d6bf8..dbf5fc15007259 100644
--- a/homeassistant/components/iota/__init__.py
+++ b/homeassistant/components/iota/__init__.py
@@ -72,8 +72,7 @@ def name(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
- attr = {CONF_WALLET_NAME: self._name}
- return attr
+ return {CONF_WALLET_NAME: self._name}
@property
def api(self):
diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json
index 5b325938411fe1..e3825bd4b2729e 100644
--- a/homeassistant/components/ipma/strings.json
+++ b/homeassistant/components/ipma/strings.json
@@ -5,9 +5,9 @@
"title": "Location",
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
"data": {
- "name": "Name",
- "latitude": "Latitude",
- "longitude": "Longitude",
+ "name": "[%key:common::config_flow::data::name%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
"mode": "Mode"
}
}
diff --git a/homeassistant/components/ipma/translations/et.json b/homeassistant/components/ipma/translations/et.json
new file mode 100644
index 00000000000000..32fab1be8dfcd8
--- /dev/null
+++ b/homeassistant/components/ipma/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ },
+ "title": "Asukoht"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/it.json b/homeassistant/components/ipma/translations/it.json
index 5ab6af8d2183df..ceee3c9091fed9 100644
--- a/homeassistant/components/ipma/translations/it.json
+++ b/homeassistant/components/ipma/translations/it.json
@@ -7,7 +7,7 @@
"user": {
"data": {
"latitude": "Latitudine",
- "longitude": "Longitudine",
+ "longitude": "Logitudine",
"mode": "Modalit\u00e0",
"name": "Nome"
},
diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json
index 09c2424151f74f..d13d281cef31f4 100644
--- a/homeassistant/components/ipp/strings.json
+++ b/homeassistant/components/ipp/strings.json
@@ -9,8 +9,8 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"base_path": "Relative path to the printer",
- "ssl": "Printer supports communication over SSL/TLS",
- "verify_ssl": "Printer uses a proper SSL certificate"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"zeroconf_confirm": {
diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json
index 6e3185767d5b36..32a04679eee127 100644
--- a/homeassistant/components/ipp/translations/ca.json
+++ b/homeassistant/components/ipp/translations/ca.json
@@ -20,8 +20,8 @@
"base_path": "Ruta relativa a la impressora",
"host": "Amfitri\u00f3",
"port": "Port",
- "ssl": "La impressora \u00e9s compatible amb comunicaci\u00f3 SSL/TLS",
- "verify_ssl": "La impressora utilitza un certificat SSL adequat"
+ "ssl": "Utilitza un certificat SSL",
+ "verify_ssl": "Verifica el certificat SSL"
},
"description": "Configura la impressora amb el protocol d'impressi\u00f3 per Internet (IPP) per integrar-la amb Home Assistant.",
"title": "Enlla\u00e7 d'impressora"
diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json
index 0267c5b5091433..7c8eccc389bbb2 100644
--- a/homeassistant/components/ipp/translations/en.json
+++ b/homeassistant/components/ipp/translations/en.json
@@ -20,8 +20,8 @@
"base_path": "Relative path to the printer",
"host": "Host",
"port": "Port",
- "ssl": "Printer supports communication over SSL/TLS",
- "verify_ssl": "Printer uses a proper SSL certificate"
+ "ssl": "Uses an SSL certificate",
+ "verify_ssl": "Verify SSL certificate"
},
"description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.",
"title": "Link your printer"
diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json
new file mode 100644
index 00000000000000..ac554145a1701d
--- /dev/null
+++ b/homeassistant/components/ipp/translations/et.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba seadistatud",
+ "connection_error": "\u00dchendumine eba\u00f5nnestus",
+ "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus kuna vajalik on \u00fchenduse uuendamine.",
+ "ipp_error": "Ilmnes IPP viga.",
+ "ipp_version_error": "Printer ei toeta seda IPP versiooni.",
+ "parse_error": "Printeri vastuse s\u00f5elumine nurjus.",
+ "unique_id_required": "Seadmel puudub avastamiseks vajalik kordumatu ID."
+ },
+ "error": {
+ "connection_error": "\u00dchendumine eba\u00f5nnestus",
+ "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovige uuesti kui SSL/TLS-i suvand on m\u00e4rgitud."
+ },
+ "flow_title": "Printer: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "base_path": "Printeri suhteline rada",
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Printer toetab SSL/TLS \u00fchendust",
+ "verify_ssl": "Printer kasutab \u00f5iget SSL-serti"
+ },
+ "description": "Seadistage oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.",
+ "title": "Linkige oma printer"
+ },
+ "zeroconf_confirm": {
+ "description": "Kas soovite seadistada {name}?",
+ "title": "Avastatud printer"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json
index c1eacf00c28da6..de8bf9bbe4f084 100644
--- a/homeassistant/components/ipp/translations/it.json
+++ b/homeassistant/components/ipp/translations/it.json
@@ -20,8 +20,8 @@
"base_path": "Percorso relativo alla stampante",
"host": "Host",
"port": "Porta",
- "ssl": "La stampante supporta la comunicazione su SSL/TLS",
- "verify_ssl": "La stampante utilizza un certificato SSL adeguato"
+ "ssl": "Utilizza un certificato SSL",
+ "verify_ssl": "Verificare il certificato SSL"
},
"description": "Configurare la stampante tramite Internet Printing Protocol (IPP) per l'integrazione con Home Assistant.",
"title": "Collegare la stampante"
diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json
index 4e94efe71c16af..3e25dcb0c4c467 100644
--- a/homeassistant/components/ipp/translations/no.json
+++ b/homeassistant/components/ipp/translations/no.json
@@ -20,8 +20,8 @@
"base_path": "Relativ bane til skriveren",
"host": "Vert",
"port": "",
- "ssl": "Skriveren st\u00f8tter kommunikasjon over SSL/TLS",
- "verify_ssl": "Skriveren bruker et riktig SSL-sertifikat"
+ "ssl": "Bruker et SSL-sertifikat",
+ "verify_ssl": "Verifisere SSL-sertifikat"
},
"description": "Sett opp skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.",
"title": "Koble til skriveren din"
diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json
index 4e5af33041c400..c5096f6af8e456 100644
--- a/homeassistant/components/ipp/translations/pl.json
+++ b/homeassistant/components/ipp/translations/pl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105 z powodu konieczno\u015bci uaktualnienia po\u0142\u0105czenia.",
"ipp_error": "Wyst\u0105pi\u0142 b\u0142\u0105d IPP.",
"ipp_version_error": "Wersja IPP nieobs\u0142ugiwana przez drukark\u0119.",
@@ -10,7 +10,7 @@
"unique_id_required": "Urz\u0105dzenie nie posiada unikalnej identyfikacji wymaganej do wykrycia."
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105. Spr\u00f3buj ponownie z zaznaczon\u0105 opcj\u0105 SSL/TLS."
},
"flow_title": "Drukarka: {name}",
diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json
index 4953c9dae5c260..9fdde4c62fec76 100644
--- a/homeassistant/components/ipp/translations/ru.json
+++ b/homeassistant/components/ipp/translations/ru.json
@@ -20,8 +20,8 @@
"base_path": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443",
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u044c \u043f\u043e SSL/TLS",
- "verify_ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.",
"title": "Internet Printing Protocol (IPP)"
diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json
index d1c9c00bdb8fe2..3ddd1250f1538b 100644
--- a/homeassistant/components/ipp/translations/zh-Hant.json
+++ b/homeassistant/components/ipp/translations/zh-Hant.json
@@ -20,8 +20,8 @@
"base_path": "\u5370\u8868\u6a5f\u76f8\u5c0d\u8def\u5f91",
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u5370\u8868\u6a5f\u652f\u63f4 SSL/TLS \u901a\u8a0a",
- "verify_ssl": "\u5370\u8868\u6a5f\u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"description": "\u900f\u904e\u7db2\u969b\u7db2\u8def\u5217\u5370\u5354\u5b9a\uff08IPP\uff09\u8a2d\u5b9a\u5370\u8868\u6a5f\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002",
"title": "\u9023\u7d50\u5370\u8868\u6a5f"
diff --git a/homeassistant/components/iqvia/strings.json b/homeassistant/components/iqvia/strings.json
index b0d82430ef7978..7e16d3ae6e38e2 100644
--- a/homeassistant/components/iqvia/strings.json
+++ b/homeassistant/components/iqvia/strings.json
@@ -13,7 +13,7 @@
"invalid_zip_code": "ZIP code is invalid"
},
"abort": {
- "already_configured": "This ZIP code has already been configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}
diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json
index f3dd4f82b6266b..f6a914bd07d9f0 100644
--- a/homeassistant/components/iqvia/translations/ko.json
+++ b/homeassistant/components/iqvia/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc774 \uc6b0\ud3b8 \ubc88\ud638\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
"error": {
"invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
diff --git a/homeassistant/components/iqvia/translations/pl.json b/homeassistant/components/iqvia/translations/pl.json
index f33a5257a609ba..e18f195d9b7b97 100644
--- a/homeassistant/components/iqvia/translations/pl.json
+++ b/homeassistant/components/iqvia/translations/pl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ten kod pocztowy jest ju\u017c skonfigurowany."
+ },
"error": {
"invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy"
},
diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py
index d45997af76f62e..065af0bd6110f3 100644
--- a/homeassistant/components/islamic_prayer_times/config_flow.py
+++ b/homeassistant/components/islamic_prayer_times/config_flow.py
@@ -23,7 +23,7 @@ def async_get_options_flow(config_entry):
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user")
diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json
index 857ce4c2dffb52..73998913f41fee 100644
--- a/homeassistant/components/islamic_prayer_times/strings.json
+++ b/homeassistant/components/islamic_prayer_times/strings.json
@@ -8,7 +8,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/ca.json b/homeassistant/components/islamic_prayer_times/translations/ca.json
index 6b01d442df8359..436534f1a3b339 100644
--- a/homeassistant/components/islamic_prayer_times/translations/ca.json
+++ b/homeassistant/components/islamic_prayer_times/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/el.json b/homeassistant/components/islamic_prayer_times/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/en.json b/homeassistant/components/islamic_prayer_times/translations/en.json
index e9781c17fb1466..e028ee8fdf65fd 100644
--- a/homeassistant/components/islamic_prayer_times/translations/en.json
+++ b/homeassistant/components/islamic_prayer_times/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Already configured. Only a single configuration possible.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/es.json b/homeassistant/components/islamic_prayer_times/translations/es.json
index 8dc6c5e5cf82ee..951ba94a7c1c4b 100644
--- a/homeassistant/components/islamic_prayer_times/translations/es.json
+++ b/homeassistant/components/islamic_prayer_times/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/et.json b/homeassistant/components/islamic_prayer_times/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/fr.json b/homeassistant/components/islamic_prayer_times/translations/fr.json
index 6499df244ab17c..6270620a412591 100644
--- a/homeassistant/components/islamic_prayer_times/translations/fr.json
+++ b/homeassistant/components/islamic_prayer_times/translations/fr.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/it.json b/homeassistant/components/islamic_prayer_times/translations/it.json
index ff1d085a58dad0..a8a5f389f90d12 100644
--- a/homeassistant/components/islamic_prayer_times/translations/it.json
+++ b/homeassistant/components/islamic_prayer_times/translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u00c8 necessaria solo una singola istanza."
+ "one_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/lb.json b/homeassistant/components/islamic_prayer_times/translations/lb.json
index 7ba934174365fd..906cb0e3c20885 100644
--- a/homeassistant/components/islamic_prayer_times/translations/lb.json
+++ b/homeassistant/components/islamic_prayer_times/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/no.json b/homeassistant/components/islamic_prayer_times/translations/no.json
index 59e601648ff176..5fc3230390ed25 100644
--- a/homeassistant/components/islamic_prayer_times/translations/no.json
+++ b/homeassistant/components/islamic_prayer_times/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/ru.json b/homeassistant/components/islamic_prayer_times/translations/ru.json
index 66f2e918f65719..696671eeb7e729 100644
--- a/homeassistant/components/islamic_prayer_times/translations/ru.json
+++ b/homeassistant/components/islamic_prayer_times/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_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.",
+ "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": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json
index b9dc6928e019af..2a2325bfc35df9 100644
--- a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json
+++ b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"user": {
diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py
index d911fae2c82fc7..e003a52c91fbed 100644
--- a/homeassistant/components/isy994/const.py
+++ b/homeassistant/components/isy994/const.py
@@ -44,7 +44,10 @@
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import (
+ AREA_SQUARE_METERS,
CONCENTRATION_PARTS_PER_MILLION,
+ CURRENCY_CENT,
+ CURRENCY_DOLLAR,
DEGREE,
ENERGY_KILO_WATT_HOUR,
FREQUENCY_HERTZ,
@@ -54,11 +57,15 @@
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
MASS_KILOGRAMS,
MASS_POUNDS,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
PRESSURE_INHG,
+ PRESSURE_MBAR,
SERVICE_LOCK,
SERVICE_UNLOCK,
SPEED_KILOMETERS_PER_HOUR,
@@ -86,6 +93,8 @@
TIME_YEARS,
UV_INDEX,
VOLT,
+ VOLUME_CUBIC_FEET,
+ VOLUME_CUBIC_METERS,
VOLUME_GALLONS,
VOLUME_LITERS,
)
@@ -316,9 +325,9 @@
"3": f"btu/{TIME_HOURS}",
"4": TEMP_CELSIUS,
"5": LENGTH_CENTIMETERS,
- "6": f"{LENGTH_FEET}³",
- "7": f"{LENGTH_FEET}³/{TIME_MINUTES}",
- "8": "m³",
+ "6": VOLUME_CUBIC_FEET,
+ "7": f"{VOLUME_CUBIC_FEET}/{TIME_MINUTES}",
+ "8": f"{VOLUME_CUBIC_METERS}",
"9": TIME_DAYS,
"10": TIME_DAYS,
"12": "dB",
@@ -344,17 +353,17 @@
"33": ENERGY_KILO_WATT_HOUR,
"34": "liedu",
"35": VOLUME_LITERS,
- "36": "lx",
+ "36": LIGHT_LUX,
"37": "mercalli",
"38": LENGTH_METERS,
- "39": f"{LENGTH_METERS}³/{TIME_HOURS}",
+ "39": f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}",
"40": SPEED_METERS_PER_SECOND,
"41": "mA",
"42": TIME_MILLISECONDS,
"43": "mV",
"44": TIME_MINUTES,
"45": TIME_MINUTES,
- "46": f"mm/{TIME_HOURS}",
+ "46": f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
"47": TIME_MONTHS,
"48": SPEED_MILES_PER_HOUR,
"49": SPEED_METERS_PER_SECOND,
@@ -377,15 +386,15 @@
"71": UV_INDEX,
"72": VOLT,
"73": POWER_WATT,
- "74": f"{POWER_WATT}/{LENGTH_METERS}²",
+ "74": f"{POWER_WATT}/{AREA_SQUARE_METERS}",
"75": "weekday",
"76": DEGREE,
"77": TIME_YEARS,
- "82": "mm",
+ "82": LENGTH_MILLIMETERS,
"83": LENGTH_KILOMETERS,
"85": "Ω",
"86": "kΩ",
- "87": f"{LENGTH_METERS}³/{LENGTH_METERS}³",
+ "87": f"{VOLUME_CUBIC_METERS}/{VOLUME_CUBIC_METERS}",
"88": "Water activity",
"89": "RPM",
"90": FREQUENCY_HERTZ,
@@ -394,10 +403,10 @@
UOM_8_BIT_RANGE: "", # Range 0-255, no unit.
UOM_DOUBLE_TEMP: UOM_DOUBLE_TEMP,
"102": "kWs",
- "103": "$",
- "104": "¢",
+ "103": CURRENCY_DOLLAR,
+ "104": CURRENCY_CENT,
"105": LENGTH_INCHES,
- "106": f"mm/{TIME_DAYS}",
+ "106": f"{LENGTH_MILLIMETERS}/{TIME_DAYS}",
"107": "", # raw 1-byte unsigned value
"108": "", # raw 2-byte unsigned value
"109": "", # raw 3-byte unsigned value
@@ -407,8 +416,8 @@
"113": "", # raw 3-byte signed value
"114": "", # raw 4-byte signed value
"116": LENGTH_MILES,
- "117": "mbar",
- "118": "hPa",
+ "117": PRESSURE_MBAR,
+ "118": PRESSURE_HPA,
"119": f"{POWER_WATT}{TIME_HOURS}",
"120": f"{LENGTH_INCHES}/{TIME_DAYS}",
}
diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json
index ce9818bc0c8616..18e08b65009417 100644
--- a/homeassistant/components/isy994/strings.json
+++ b/homeassistant/components/isy994/strings.json
@@ -6,7 +6,7 @@
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
- "host": "URL",
+ "host": "[%key:common::config_flow::data::url%]",
"password": "[%key:common::config_flow::data::password%]",
"tls": "The TLS version of the ISY controller."
},
diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json
index b9c3362c488cb3..d14dfa6c65a16a 100644
--- a/homeassistant/components/isy994/translations/de.json
+++ b/homeassistant/components/isy994/translations/de.json
@@ -10,9 +10,20 @@
"step": {
"user": {
"data": {
+ "host": "URL",
+ "password": "Passwort",
"username": "Benutzername"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ignore_string": "Zeichenfolge ignorieren"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json
index 102d1fabf052ad..5afaf2ad830e03 100644
--- a/homeassistant/components/isy994/translations/fr.json
+++ b/homeassistant/components/isy994/translations/fr.json
@@ -29,7 +29,8 @@
"data": {
"ignore_string": "Ignorer la cha\u00eene",
"restore_light_state": "Restaurer la luminosit\u00e9",
- "sensor_string": "Node Sensor String"
+ "sensor_string": "Node Sensor String",
+ "variable_sensor_string": "Cha\u00eene de capteur variable"
},
"description": "D\u00e9finir les options pour l'int\u00e9gration ISY: \n \u2022 Node Sensor String: tout p\u00e9riph\u00e9rique ou dossier contenant \u00abNode Sensor String\u00bb dans le nom sera trait\u00e9 comme un capteur ou un capteur binaire. \n \u2022 Ignore String : tout p\u00e9riph\u00e9rique avec \u00abIgnore String\u00bb dans le nom sera ignor\u00e9. \n \u2022 Variable Sensor String : toute variable contenant \u00abVariable Sensor String\u00bb sera ajout\u00e9e en tant que capteur. \n \u2022 Restaurer la luminosit\u00e9 : si cette option est activ\u00e9e, la luminosit\u00e9 pr\u00e9c\u00e9dente sera restaur\u00e9e lors de l'allumage d'une lumi\u00e8re au lieu de la fonction int\u00e9gr\u00e9e de l'appareil.",
"title": "Options ISY994"
diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json
index a39fc58dc25040..b252dde3257cab 100644
--- a/homeassistant/components/isy994/translations/nl.json
+++ b/homeassistant/components/isy994/translations/nl.json
@@ -1,5 +1,12 @@
{
"config": {
- "flow_title": "Universele apparaten ISY994 {name} ({host})"
+ "flow_title": "Universele apparaten ISY994 {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json
index 27f79ef2801b8f..d35b9e91eb6411 100644
--- a/homeassistant/components/isy994/translations/pl.json
+++ b/homeassistant/components/isy994/translations/pl.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "Urz\u0105dzenia uniwersalne ISY994 {name} ({host})",
"step": {
diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json
index 9772bd4c93c182..7d1e8f1d4768ab 100644
--- a/homeassistant/components/izone/strings.json
+++ b/homeassistant/components/izone/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of iZone is necessary.",
- "no_devices_found": "No iZone devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/izone/translations/ca.json b/homeassistant/components/izone/translations/ca.json
index 811b3cc8c29b2c..6c05ec8ce8bd89 100644
--- a/homeassistant/components/izone/translations/ca.json
+++ b/homeassistant/components/izone/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius iZone a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de iZone."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/en.json b/homeassistant/components/izone/translations/en.json
index 03cc003752b38e..f0ab3077adfc70 100644
--- a/homeassistant/components/izone/translations/en.json
+++ b/homeassistant/components/izone/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No iZone devices found on the network.",
- "single_instance_allowed": "Only a single configuration of iZone is necessary."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/it.json b/homeassistant/components/izone/translations/it.json
index 79151ca44a4cd9..c141fadc27023f 100644
--- a/homeassistant/components/izone/translations/it.json
+++ b/homeassistant/components/izone/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo iZone trovato in rete.",
- "single_instance_allowed": "\u00c8 necessaria una sola configurazione di iZone."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/no.json b/homeassistant/components/izone/translations/no.json
index 5c3e424833929b..84c50c19f77655 100644
--- a/homeassistant/components/izone/translations/no.json
+++ b/homeassistant/components/izone/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.",
- "single_instance_allowed": "Bare en konfigurasjon av iZone er n\u00f8dvendig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/ru.json b/homeassistant/components/izone/translations/ru.json
index 23b0a85370caa4..d57ba2d0fca497 100644
--- a/homeassistant/components/izone/translations/ru.json
+++ b/homeassistant/components/izone/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 iZone \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/zh-Hant.json b/homeassistant/components/izone/translations/zh-Hant.json
index 283136b5c8657b..f49de8669d10c6 100644
--- a/homeassistant/components/izone/translations/zh-Hant.json
+++ b/homeassistant/components/izone/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 iZone \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 iZone \u5373\u53ef\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index 606f4fffab12f8..7b3a3af9c1185a 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -113,10 +113,9 @@ def make_zmanim(self, date):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- if self._type == "holiday":
- return self._holiday_attrs
-
- return {}
+ if self._type != "holiday":
+ return {}
+ return self._holiday_attrs
def get_state(self, daytime_date, after_shkia_date, after_tzais_date):
"""For a given type of sensor, return the state."""
diff --git a/homeassistant/components/juicenet/strings.json b/homeassistant/components/juicenet/strings.json
index 4c8ffb8c62f931..bc4a66e72d4361 100644
--- a/homeassistant/components/juicenet/strings.json
+++ b/homeassistant/components/juicenet/strings.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "This JuiceNet account is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API Token"
+ "api_token": "[%key:common::config_flow::data::api_token%]"
},
"description": "You will need the API Token from https://home.juice.net/Manage.",
"title": "Connect to JuiceNet"
diff --git a/homeassistant/components/juicenet/translations/ca.json b/homeassistant/components/juicenet/translations/ca.json
index 1d7cad2111be77..f5df69210620e3 100644
--- a/homeassistant/components/juicenet/translations/ca.json
+++ b/homeassistant/components/juicenet/translations/ca.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Aquest compte de JuiceNet ja est\u00e0 configurat"
+ "already_configured": "El compte ja ha estat configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"step": {
"user": {
"data": {
- "api_token": "Token de l'API de JuiceNet"
+ "api_token": "Token d'API"
},
"description": "Necessitar\u00e0s la clau API de https://home.juice.net/Manage.",
"title": "Connexi\u00f3 amb JuiceNet"
diff --git a/homeassistant/components/juicenet/translations/en.json b/homeassistant/components/juicenet/translations/en.json
index faf21a8d6174bb..3d11f5de63a682 100644
--- a/homeassistant/components/juicenet/translations/en.json
+++ b/homeassistant/components/juicenet/translations/en.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "This JuiceNet account is already configured"
+ "already_configured": "Account is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API Token"
+ "api_token": "API Token"
},
"description": "You will need the API Token from https://home.juice.net/Manage.",
"title": "Connect to JuiceNet"
diff --git a/homeassistant/components/juicenet/translations/it.json b/homeassistant/components/juicenet/translations/it.json
index be8eee9745d88a..90e3ccd3c17f5b 100644
--- a/homeassistant/components/juicenet/translations/it.json
+++ b/homeassistant/components/juicenet/translations/it.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Questo account JuiceNet \u00e8 gi\u00e0 configurato"
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"step": {
"user": {
"data": {
- "api_token": "Token API JuiceNet"
+ "api_token": "Token API"
},
"description": "Avrete bisogno del Token API da https://home.juice.net/Manage.",
"title": "Connettersi a JuiceNet"
diff --git a/homeassistant/components/juicenet/translations/no.json b/homeassistant/components/juicenet/translations/no.json
index 1d0e3a15f5bc90..790dea03a02a1b 100644
--- a/homeassistant/components/juicenet/translations/no.json
+++ b/homeassistant/components/juicenet/translations/no.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Denne JuiceNet-kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API-token"
+ "api_token": "API-token"
},
"description": "Du trenger API-tokenet fra https://home.juice.net/Manage.",
"title": "Koble til JuiceNet"
diff --git a/homeassistant/components/juicenet/translations/pl.json b/homeassistant/components/juicenet/translations/pl.json
index 601ce0c91284ec..c308ad2524a81f 100644
--- a/homeassistant/components/juicenet/translations/pl.json
+++ b/homeassistant/components/juicenet/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/juicenet/translations/ru.json b/homeassistant/components/juicenet/translations/ru.json
index 69b68f82990b47..2fec7d485c477c 100644
--- a/homeassistant/components/juicenet/translations/ru.json
+++ b/homeassistant/components/juicenet/translations/ru.json
@@ -4,7 +4,7 @@
"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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/juicenet/translations/zh-Hant.json b/homeassistant/components/juicenet/translations/zh-Hant.json
index b54bb3d4676fbc..815edb1fb2710b 100644
--- a/homeassistant/components/juicenet/translations/zh-Hant.json
+++ b/homeassistant/components/juicenet/translations/zh-Hant.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "JuiceNet \u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API \u5bc6\u9470"
+ "api_token": "API \u5bc6\u9470"
},
"description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u5bc6\u9470\u3002",
"title": "\u9023\u7dda\u81f3 JuiceNet"
diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py
index ce179515e880f2..d1a616f86fa83f 100644
--- a/homeassistant/components/kankun/switch.py
+++ b/homeassistant/components/kankun/switch.py
@@ -94,11 +94,6 @@ def _query_state(self):
except requests.RequestException:
_LOGGER.error("State query failed")
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def name(self):
"""Return the name of the switch."""
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index 5a2f29e6247fd1..87ade4955a010e 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -6,7 +6,12 @@
from xknx.devices import DateTime, ExposeSensor
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import XKNXException
-from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType
+from xknx.io import (
+ DEFAULT_MCAST_GRP,
+ DEFAULT_MCAST_PORT,
+ ConnectionConfig,
+ ConnectionType,
+)
from xknx.telegram import AddressFilter, GroupAddress, Telegram
from homeassistant.const import (
@@ -24,7 +29,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
-from .const import DATA_KNX, DOMAIN, SupportedPlatforms
+from .const import DOMAIN, SupportedPlatforms
from .factory import create_knx_device
from .schema import (
BinarySensorSchema,
@@ -48,6 +53,9 @@
CONF_KNX_TUNNELING = "tunneling"
CONF_KNX_FIRE_EVENT = "fire_event"
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
+CONF_KNX_INDIVIDUAL_ADDRESS = "individual_address"
+CONF_KNX_MCAST_GRP = "multicast_group"
+CONF_KNX_MCAST_PORT = "multicast_port"
CONF_KNX_STATE_UPDATER = "state_updater"
CONF_KNX_RATE_LIMIT = "rate_limit"
CONF_KNX_EXPOSE = "expose"
@@ -72,6 +80,11 @@
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, "fire_ev"): vol.All(
cv.ensure_list, [cv.string]
),
+ vol.Optional(
+ CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
+ ): cv.string,
+ vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string,
+ vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port,
vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean,
vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100)
@@ -126,21 +139,19 @@
async def async_setup(hass, config):
"""Set up the KNX component."""
try:
- hass.data[DATA_KNX] = KNXModule(hass, config)
- hass.data[DATA_KNX].async_create_exposures()
- await hass.data[DATA_KNX].start()
+ hass.data[DOMAIN] = KNXModule(hass, config)
+ hass.data[DOMAIN].async_create_exposures()
+ await hass.data[DOMAIN].start()
except XKNXException as ex:
- _LOGGER.warning("Can't connect to KNX interface: %s", ex)
+ _LOGGER.warning("Could not connect to KNX interface: %s", ex)
hass.components.persistent_notification.async_create(
- f"Can't connect to KNX interface:
{ex}", title="KNX"
+ f"Could not connect to KNX interface:
{ex}", title="KNX"
)
for platform in SupportedPlatforms:
if platform.value in config[DOMAIN]:
for device_config in config[DOMAIN][platform.value]:
- create_knx_device(
- hass, platform, hass.data[DATA_KNX].xknx, device_config
- )
+ create_knx_device(platform, hass.data[DOMAIN].xknx, device_config)
# We need to wait until all entities are loaded into the device list since they could also be created from other platforms
for platform in SupportedPlatforms:
@@ -148,7 +159,7 @@ async def async_setup(hass, config):
discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config)
)
- if not hass.data[DATA_KNX].xknx.devices:
+ if not hass.data[DOMAIN].xknx.devices:
_LOGGER.warning(
"No KNX devices are configured. Please read "
"https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes"
@@ -157,7 +168,7 @@ async def async_setup(hass, config):
hass.services.async_register(
DOMAIN,
SERVICE_KNX_SEND,
- hass.data[DATA_KNX].service_send_to_knx_bus,
+ hass.data[DOMAIN].service_send_to_knx_bus,
schema=SERVICE_KNX_SEND_SCHEMA,
)
@@ -180,17 +191,17 @@ def init_xknx(self):
"""Initialize of KNX object."""
self.xknx = XKNX(
config=self.config_file(),
- loop=self.hass.loop,
+ own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS],
rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT],
+ multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP],
+ multicast_port=self.config[DOMAIN][CONF_KNX_MCAST_PORT],
+ connection_config=self.connection_config(),
+ state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
)
async def start(self):
"""Start KNX object. Connect to tunneling or Routing device."""
- connection_config = self.connection_config()
- await self.xknx.start(
- state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
- connection_config=connection_config,
- )
+ await self.xknx.start()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.connected = True
@@ -213,9 +224,8 @@ def connection_config(self):
return self.connection_config_tunneling()
if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
- # return None to let xknx use config from xknx.yaml connection block if given
- # otherwise it will use default ConnectionConfig (Automatic)
- return None
+ # config from xknx.yaml always has priority later on
+ return ConnectionConfig()
def connection_config_routing(self):
"""Return the connection_config if routing is configured."""
@@ -229,12 +239,10 @@ def connection_config_routing(self):
def connection_config_tunneling(self):
"""Return the connection_config if tunneling is configured."""
gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST]
- gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT)
+ gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT]
local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(
ConnectionSchema.CONF_KNX_LOCAL_IP
)
- if gateway_port is None:
- gateway_port = DEFAULT_MCAST_PORT
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=gateway_ip,
@@ -267,7 +275,7 @@ def async_create_exposures(self):
attribute = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE)
default = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
address = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS)
- if expose_type in ["time", "date", "datetime"]:
+ if expose_type.lower() in ["time", "date", "datetime"]:
exposure = KNXExposeTime(self.xknx, expose_type, address)
exposure.async_register()
self.exposures.append(exposure)
@@ -313,29 +321,29 @@ def calculate_payload(attr_payload):
payload = calculate_payload(attr_payload)
address = GroupAddress(attr_address)
- telegram = Telegram()
- telegram.payload = payload
- telegram.group_address = address
+ telegram = Telegram(group_address=address, payload=payload)
await self.xknx.telegrams.put(telegram)
class KNXExposeTime:
"""Object to Expose Time/Date object to KNX bus."""
- def __init__(self, xknx, expose_type, address):
+ def __init__(self, xknx: XKNX, expose_type: str, address: str):
"""Initialize of Expose class."""
self.xknx = xknx
- self.type = expose_type
+ self.expose_type = expose_type
self.address = address
self.device = None
@callback
def async_register(self):
"""Register listener."""
- broadcast_type_string = self.type.upper()
- broadcast_type = broadcast_type_string
self.device = DateTime(
- self.xknx, "Time", broadcast_type=broadcast_type, group_address=self.address
+ self.xknx,
+ name=self.expose_type.capitalize(),
+ broadcast_type=self.expose_type.upper(),
+ localtime=True,
+ group_address=self.address,
)
diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py
index f3b7e881134252..a62e95f1defa19 100644
--- a/homeassistant/components/knx/binary_sensor.py
+++ b/homeassistant/components/knx/binary_sensor.py
@@ -1,67 +1,43 @@
"""Support for KNX/IP binary sensors."""
+from typing import Any, Dict, Optional
+
from xknx.devices import BinarySensor as XknxBinarySensor
-from homeassistant.components.binary_sensor import BinarySensorEntity
-from homeassistant.core import callback
+from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity
-from . import DATA_KNX
+from .const import ATTR_COUNTER, DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxBinarySensor):
entities.append(KNXBinarySensor(device))
async_add_entities(entities)
-class KNXBinarySensor(BinarySensorEntity):
+class KNXBinarySensor(KnxEntity, BinarySensorEntity):
"""Representation of a KNX binary sensor."""
def __init__(self, device: XknxBinarySensor):
"""Initialize of KNX binary sensor."""
- self.device = device
-
- @callback
- def async_register_callbacks(self):
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
-
- self.device.register_device_updated_cb(after_update_callback)
-
- async def async_added_to_hass(self):
- """Store register state change callback."""
- self.async_register_callbacks()
-
- async def async_update(self):
- """Request a state update from KNX bus."""
- await self.device.sync()
-
- @property
- def name(self):
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self):
- """No polling needed within KNX."""
- return False
+ super().__init__(device)
@property
def device_class(self):
"""Return the class of this sensor."""
- return self.device.device_class
+ if self._device.device_class in DEVICE_CLASSES:
+ return self._device.device_class
+ return None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
- return self.device.is_on()
+ return self._device.is_on()
+
+ @property
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ """Return device specific state attributes."""
+ return {ATTR_COUNTER: self._device.counter}
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index b5aaeb679073a8..1960627a8d6bb5 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -14,8 +14,8 @@
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
-from . import DATA_KNX
-from .const import OPERATION_MODES, PRESET_MODES
+from .const import DOMAIN, OPERATION_MODES, PRESET_MODES
+from .knx_entity import KnxEntity
OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items())
PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items())
@@ -24,18 +24,19 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up climate(s) for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxClimate):
entities.append(KNXClimate(device))
async_add_entities(entities)
-class KNXClimate(ClimateEntity):
+class KNXClimate(KnxEntity, ClimateEntity):
"""Representation of a KNX climate device."""
def __init__(self, device: XknxClimate):
"""Initialize of a KNX climate device."""
- self.device = device
+ super().__init__(device)
+
self._unit_of_measurement = TEMP_CELSIUS
@property
@@ -43,35 +44,10 @@ def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
- async def async_added_to_hass(self) -> None:
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
-
- self.device.register_device_updated_cb(after_update_callback)
- self.device.mode.register_device_updated_cb(after_update_callback)
-
async def async_update(self):
"""Request a state update from KNX bus."""
- await self.device.sync()
- await self.device.mode.sync()
-
- @property
- def name(self) -> str:
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self) -> bool:
- """Return True if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self) -> bool:
- """No polling needed within KNX."""
- return False
+ await self._device.sync()
+ await self._device.mode.sync()
@property
def temperature_unit(self):
@@ -81,44 +57,44 @@ def temperature_unit(self):
@property
def current_temperature(self):
"""Return the current temperature."""
- return self.device.temperature.value
+ return self._device.temperature.value
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
- return self.device.temperature_step
+ return self._device.temperature_step
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
- return self.device.target_temperature.value
+ return self._device.target_temperature.value
@property
def min_temp(self):
"""Return the minimum temperature."""
- return self.device.target_temperature_min
+ return self._device.target_temperature_min
@property
def max_temp(self):
"""Return the maximum temperature."""
- return self.device.target_temperature_max
+ return self._device.target_temperature_max
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
- await self.device.set_target_temperature(temperature)
+ await self._device.set_target_temperature(temperature)
self.async_write_ha_state()
@property
def hvac_mode(self) -> Optional[str]:
"""Return current operation ie. heat, cool, idle."""
- if self.device.supports_on_off and not self.device.is_on:
+ if self._device.supports_on_off and not self._device.is_on:
return HVAC_MODE_OFF
- if self.device.mode.supports_operation_mode:
+ if self._device.mode.supports_operation_mode:
return OPERATION_MODES.get(
- self.device.mode.operation_mode.value, HVAC_MODE_HEAT
+ self._device.mode.operation_mode.value, HVAC_MODE_HEAT
)
# default to "heat"
return HVAC_MODE_HEAT
@@ -128,10 +104,10 @@ def hvac_modes(self) -> Optional[List[str]]:
"""Return the list of available operation modes."""
_operations = [
OPERATION_MODES.get(operation_mode.value)
- for operation_mode in self.device.mode.operation_modes
+ for operation_mode in self._device.mode.operation_modes
]
- if self.device.supports_on_off:
+ if self._device.supports_on_off:
if not _operations:
_operations.append(HVAC_MODE_HEAT)
_operations.append(HVAC_MODE_OFF)
@@ -142,16 +118,16 @@ def hvac_modes(self) -> Optional[List[str]]:
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set operation mode."""
- if self.device.supports_on_off and hvac_mode == HVAC_MODE_OFF:
- await self.device.turn_off()
+ if self._device.supports_on_off and hvac_mode == HVAC_MODE_OFF:
+ await self._device.turn_off()
else:
- if self.device.supports_on_off and not self.device.is_on:
- await self.device.turn_on()
- if self.device.mode.supports_operation_mode:
+ if self._device.supports_on_off and not self._device.is_on:
+ await self._device.turn_on()
+ if self._device.mode.supports_operation_mode:
knx_operation_mode = HVACOperationMode(
OPERATION_MODES_INV.get(hvac_mode)
)
- await self.device.mode.set_operation_mode(knx_operation_mode)
+ await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state()
@property
@@ -160,8 +136,8 @@ def preset_mode(self) -> Optional[str]:
Requires SUPPORT_PRESET_MODE.
"""
- if self.device.mode.supports_operation_mode:
- return PRESET_MODES.get(self.device.mode.operation_mode.value, PRESET_AWAY)
+ if self._device.mode.supports_operation_mode:
+ return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY)
return None
@property
@@ -172,14 +148,14 @@ def preset_modes(self) -> Optional[List[str]]:
"""
_presets = [
PRESET_MODES.get(operation_mode.value)
- for operation_mode in self.device.mode.operation_modes
+ for operation_mode in self._device.mode.operation_modes
]
return list(filter(None, _presets))
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
- if self.device.mode.supports_operation_mode:
+ if self._device.mode.supports_operation_mode:
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode))
- await self.device.mode.set_operation_mode(knx_operation_mode)
+ await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state()
diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py
index a81fc526415e4f..8b0dd90393b91a 100644
--- a/homeassistant/components/knx/const.py
+++ b/homeassistant/components/knx/const.py
@@ -15,7 +15,6 @@
)
DOMAIN = "knx"
-DATA_KNX = "data_knx"
CONF_STATE_ADDRESS = "state_address"
CONF_SYNC_STATE = "sync_state"
@@ -60,3 +59,5 @@ class SupportedPlatforms(Enum):
"Standby": PRESET_AWAY,
"Comfort": PRESET_COMFORT,
}
+
+ATTR_COUNTER = "counter"
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 8c50bb2afe9cc8..c677b12c0ee986 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -15,65 +15,39 @@
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_utc_time_change
-from . import DATA_KNX
+from .const import DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up cover(s) for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxCover):
entities.append(KNXCover(device))
async_add_entities(entities)
-class KNXCover(CoverEntity):
+class KNXCover(KnxEntity, CoverEntity):
"""Representation of a KNX cover."""
def __init__(self, device: XknxCover):
"""Initialize the cover."""
- self.device = device
+ super().__init__(device)
+
self._unsubscribe_auto_updater = None
@callback
- def async_register_callbacks(self):
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
- if self.device.is_traveling():
- self.start_auto_updater()
-
- self.device.register_device_updated_cb(after_update_callback)
-
- async def async_added_to_hass(self):
- """Store register state change callback."""
- self.async_register_callbacks()
-
- async def async_update(self):
- """Request a state update from KNX bus."""
- await self.device.sync()
-
- @property
- def name(self):
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self):
- """No polling needed within KNX."""
- return False
+ async def after_update_callback(self, device):
+ """Call after device was updated."""
+ self.async_write_ha_state()
+ if self._device.is_traveling():
+ self.start_auto_updater()
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- if self.device.supports_angle:
+ if self._device.supports_angle:
return DEVICE_CLASS_BLIND
return None
@@ -81,9 +55,9 @@ def device_class(self):
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
- if self.device.supports_stop:
+ if self._device.supports_stop:
supported_features |= SUPPORT_STOP
- if self.device.supports_angle:
+ if self._device.supports_angle:
supported_features |= SUPPORT_SET_TILT_POSITION
return supported_features
@@ -95,57 +69,57 @@ def current_cover_position(self):
"""
# In KNX 0 is open, 100 is closed.
try:
- return 100 - self.device.current_position()
+ return 100 - self._device.current_position()
except TypeError:
return None
@property
def is_closed(self):
"""Return if the cover is closed."""
- return self.device.is_closed()
+ return self._device.is_closed()
@property
def is_opening(self):
"""Return if the cover is opening or not."""
- return self.device.is_opening()
+ return self._device.is_opening()
@property
def is_closing(self):
"""Return if the cover is closing or not."""
- return self.device.is_closing()
+ return self._device.is_closing()
async def async_close_cover(self, **kwargs):
"""Close the cover."""
- await self.device.set_down()
+ await self._device.set_down()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
- await self.device.set_up()
+ await self._device.set_up()
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
knx_position = 100 - kwargs[ATTR_POSITION]
- await self.device.set_position(knx_position)
+ await self._device.set_position(knx_position)
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
- await self.device.stop()
+ await self._device.stop()
self.stop_auto_updater()
@property
def current_cover_tilt_position(self):
"""Return current tilt position of cover."""
- if not self.device.supports_angle:
+ if not self._device.supports_angle:
return None
try:
- return 100 - self.device.current_angle()
+ return 100 - self._device.current_angle()
except TypeError:
return None
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION]
- await self.device.set_angle(knx_tilt_position)
+ await self._device.set_angle(knx_tilt_position)
def start_auto_updater(self):
"""Start the autoupdater to update Home Assistant while cover is moving."""
@@ -164,7 +138,7 @@ def stop_auto_updater(self):
def auto_updater_hook(self, now):
"""Call for the autoupdater."""
self.async_write_ha_state()
- if self.device.position_reached():
+ if self._device.position_reached():
self.stop_auto_updater()
- self.hass.add_job(self.device.auto_stop_if_necessary())
+ self.hass.add_job(self._device.auto_stop_if_necessary())
diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py
index 42c4dd675f5c76..3334e49ce38cae 100644
--- a/homeassistant/components/knx/factory.py
+++ b/homeassistant/components/knx/factory.py
@@ -1,7 +1,6 @@
"""Factory function to initialize KNX devices from config."""
from xknx import XKNX
from xknx.devices import (
- ActionCallback as XknxActionCallback,
BinarySensor as XknxBinarySensor,
Climate as XknxClimate,
ClimateMode as XknxClimateMode,
@@ -16,11 +15,9 @@
)
from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN, ColorTempModes, SupportedPlatforms
+from .const import ColorTempModes, SupportedPlatforms
from .schema import (
BinarySensorSchema,
ClimateSchema,
@@ -34,7 +31,6 @@
def create_knx_device(
- hass: HomeAssistant,
platform: SupportedPlatforms,
knx_module: XKNX,
config: ConfigType,
@@ -62,7 +58,7 @@ def create_knx_device(
return _create_scene(knx_module, config)
if platform is SupportedPlatforms.binary_sensor:
- return _create_binary_sensor(hass, knx_module, config)
+ return _create_binary_sensor(knx_module, config)
if platform is SupportedPlatforms.weather:
return _create_weather(knx_module, config)
@@ -239,24 +235,9 @@ def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene:
)
-def _create_binary_sensor(
- hass: HomeAssistant, knx_module: XKNX, config: ConfigType
-) -> XknxBinarySensor:
+def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySensor:
"""Return a KNX binary sensor to be used within XKNX."""
device_name = config[CONF_NAME]
- actions = []
- automations = config.get(BinarySensorSchema.CONF_AUTOMATION)
- if automations is not None:
- for automation in automations:
- counter = automation[BinarySensorSchema.CONF_COUNTER]
- hook = automation[BinarySensorSchema.CONF_HOOK]
- action = automation[BinarySensorSchema.CONF_ACTION]
- script_name = f"{device_name} turn ON script"
- script = Script(hass, action, script_name, DOMAIN)
- action = XknxActionCallback(
- knx_module, script.async_run, hook=hook, counter=counter
- )
- actions.append(action)
return XknxBinarySensor(
knx_module,
@@ -265,8 +246,8 @@ def _create_binary_sensor(
sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
device_class=config.get(CONF_DEVICE_CLASS),
ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE],
+ context_timeout=config[BinarySensorSchema.CONF_CONTEXT_TIMEOUT],
reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER),
- actions=actions,
)
@@ -287,6 +268,9 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather:
group_address_brightness_west=config.get(
WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS
),
+ group_address_brightness_north=config.get(
+ WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS
+ ),
group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS),
group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS),
group_address_frost_alarm=config.get(
diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py
new file mode 100644
index 00000000000000..296bcb2f5405fb
--- /dev/null
+++ b/homeassistant/components/knx/knx_entity.py
@@ -0,0 +1,51 @@
+"""Base class for KNX devices."""
+from xknx.devices import Climate as XknxClimate, Device as XknxDevice
+
+from homeassistant.helpers.entity import Entity
+
+from .const import DOMAIN
+
+
+class KnxEntity(Entity):
+ """Representation of a KNX entity."""
+
+ def __init__(self, device: XknxDevice):
+ """Set up device."""
+ self._device = device
+
+ @property
+ def name(self):
+ """Return the name of the KNX device."""
+ return self._device.name
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self.hass.data[DOMAIN].connected
+
+ @property
+ def should_poll(self):
+ """No polling needed within KNX."""
+ return False
+
+ async def async_update(self):
+ """Request a state update from KNX bus."""
+ await self._device.sync()
+
+ async def after_update_callback(self, device: XknxDevice):
+ """Call after device was updated."""
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self) -> None:
+ """Store register state change callback."""
+ self._device.register_device_updated_cb(self.after_update_callback)
+
+ if isinstance(self._device, XknxClimate):
+ self._device.mode.register_device_updated_cb(self.after_update_callback)
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect device object when removed."""
+ self._device.unregister_device_updated_cb(self.after_update_callback)
+
+ if isinstance(self._device, XknxClimate):
+ self._device.mode.unregister_device_updated_cb(self.after_update_callback)
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index 6d8438df0f92f9..d9f0f9c0d3afe6 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -12,10 +12,10 @@
SUPPORT_WHITE_VALUE,
LightEntity,
)
-from homeassistant.core import callback
import homeassistant.util.color as color_util
-from . import DATA_KNX
+from .const import DOMAIN
+from .knx_entity import KnxEntity
DEFAULT_COLOR = (0.0, 0.0)
DEFAULT_BRIGHTNESS = 255
@@ -25,18 +25,18 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up lights for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxLight):
entities.append(KNXLight(device))
async_add_entities(entities)
-class KNXLight(LightEntity):
+class KNXLight(KnxEntity, LightEntity):
"""Representation of a KNX light."""
def __init__(self, device: XknxLight):
"""Initialize of KNX light."""
- self.device = device
+ super().__init__(device)
self._min_kelvin = device.min_kelvin
self._max_kelvin = device.max_kelvin
@@ -47,46 +47,13 @@ def __init__(self, device: XknxLight):
self._min_kelvin
)
- @callback
- def async_register_callbacks(self):
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
-
- self.device.register_device_updated_cb(after_update_callback)
-
- async def async_added_to_hass(self):
- """Store register state change callback."""
- self.async_register_callbacks()
-
- async def async_update(self):
- """Request a state update from KNX bus."""
- await self.device.sync()
-
- @property
- def name(self):
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self):
- """No polling needed within KNX."""
- return False
-
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
- if self.device.supports_brightness:
- return self.device.current_brightness
+ if self._device.supports_brightness:
+ return self._device.current_brightness
hsv_color = self._hsv_color
- if self.device.supports_color and hsv_color:
+ if self._device.supports_color and hsv_color:
return round(hsv_color[-1] / 100 * 255)
return None
@@ -94,35 +61,35 @@ def brightness(self):
def hs_color(self):
"""Return the HS color value."""
rgb = None
- if self.device.supports_rgbw or self.device.supports_color:
- rgb, _ = self.device.current_color
+ if self._device.supports_rgbw or self._device.supports_color:
+ rgb, _ = self._device.current_color
return color_util.color_RGB_to_hs(*rgb) if rgb else None
@property
def _hsv_color(self):
"""Return the HSV color value."""
rgb = None
- if self.device.supports_rgbw or self.device.supports_color:
- rgb, _ = self.device.current_color
+ if self._device.supports_rgbw or self._device.supports_color:
+ rgb, _ = self._device.current_color
return color_util.color_RGB_to_hsv(*rgb) if rgb else None
@property
def white_value(self):
"""Return the white value."""
white = None
- if self.device.supports_rgbw:
- _, white = self.device.current_color
+ if self._device.supports_rgbw:
+ _, white = self._device.current_color
return white
@property
def color_temp(self):
"""Return the color temperature in mireds."""
- if self.device.supports_color_temperature:
- kelvin = self.device.current_color_temperature
+ if self._device.supports_color_temperature:
+ kelvin = self._device.current_color_temperature
if kelvin is not None:
return color_util.color_temperature_kelvin_to_mired(kelvin)
- if self.device.supports_tunable_white:
- relative_ct = self.device.current_tunable_white
+ if self._device.supports_tunable_white:
+ relative_ct = self._device.current_tunable_white
if relative_ct is not None:
# as KNX devices typically use Kelvin we use it as base for
# calculating ct from percent
@@ -155,19 +122,22 @@ def effect(self):
@property
def is_on(self):
"""Return true if light is on."""
- return self.device.state
+ return self._device.state
@property
def supported_features(self):
"""Flag supported features."""
flags = 0
- if self.device.supports_brightness:
+ if self._device.supports_brightness:
flags |= SUPPORT_BRIGHTNESS
- if self.device.supports_color:
+ if self._device.supports_color:
flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS
- if self.device.supports_rgbw:
+ if self._device.supports_rgbw:
flags |= SUPPORT_COLOR | SUPPORT_WHITE_VALUE
- if self.device.supports_color_temperature or self.device.supports_tunable_white:
+ if (
+ self._device.supports_color_temperature
+ or self._device.supports_tunable_white
+ ):
flags |= SUPPORT_COLOR_TEMP
return flags
@@ -191,14 +161,16 @@ async def async_turn_on(self, **kwargs):
or update_white_value
or update_color_temp
):
- await self.device.set_on()
+ await self._device.set_on()
- if self.device.supports_brightness and (update_brightness and not update_color):
+ if self._device.supports_brightness and (
+ update_brightness and not update_color
+ ):
# if we don't need to update the color, try updating brightness
# directly if supported; don't do it if color also has to be
# changed, as RGB color implicitly sets the brightness as well
- await self.device.set_brightness(brightness)
- elif (self.device.supports_rgbw or self.device.supports_color) and (
+ await self._device.set_brightness(brightness)
+ elif (self._device.supports_rgbw or self._device.supports_color) and (
update_brightness or update_color or update_white_value
):
# change RGB color, white value (if supported), and brightness
@@ -208,25 +180,25 @@ async def async_turn_on(self, **kwargs):
brightness = DEFAULT_BRIGHTNESS
if hs_color is None:
hs_color = DEFAULT_COLOR
- if white_value is None and self.device.supports_rgbw:
+ if white_value is None and self._device.supports_rgbw:
white_value = DEFAULT_WHITE_VALUE
rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)
- await self.device.set_color(rgb, white_value)
+ await self._device.set_color(rgb, white_value)
if update_color_temp:
kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds))
kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin))
- if self.device.supports_color_temperature:
- await self.device.set_color_temperature(kelvin)
- elif self.device.supports_tunable_white:
+ if self._device.supports_color_temperature:
+ await self._device.set_color_temperature(kelvin)
+ elif self._device.supports_tunable_white:
relative_ct = int(
255
* (kelvin - self._min_kelvin)
/ (self._max_kelvin - self._min_kelvin)
)
- await self.device.set_tunable_white(relative_ct)
+ await self._device.set_tunable_white(relative_ct)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
- await self.device.set_off()
+ await self._device.set_off()
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 8986d85b8b6f5a..2d387f0653de14 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -2,6 +2,7 @@
"domain": "knx",
"name": "KNX",
"documentation": "https://www.home-assistant.io/integrations/knx",
- "requirements": ["xknx==0.13.0"],
- "codeowners": ["@Julius2342", "@farmio", "@marvin-w"]
+ "requirements": ["xknx==0.15.0"],
+ "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
+ "quality_scale": "silver"
}
diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py
index e47cfca2794e77..7210795bd71ab8 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -5,13 +5,13 @@
from homeassistant.components.notify import BaseNotificationService
-from . import DATA_KNX
+from .const import DOMAIN
async def async_get_service(hass, config, discovery_info=None):
"""Get the KNX notification service."""
notification_devices = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxNotification):
notification_devices.append(device)
return (
diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py
index b4df94a0fd4d34..6c76fdbd199e2b 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -5,30 +5,26 @@
from homeassistant.components.scene import Scene
-from . import DATA_KNX
+from .const import DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the scenes for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxScene):
entities.append(KNXScene(device))
async_add_entities(entities)
-class KNXScene(Scene):
+class KNXScene(KnxEntity, Scene):
"""Representation of a KNX scene."""
- def __init__(self, scene: XknxScene):
+ def __init__(self, device: XknxScene):
"""Init KNX scene."""
- self.scene = scene
-
- @property
- def name(self):
- """Return the name of the scene."""
- return self.scene.name
+ super().__init__(device)
async def async_activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
- await self.scene.run()
+ await self._device.run()
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index a436f2dcdc8e7a..84a54536db5ddb 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -1,6 +1,7 @@
"""Voluptuous schemas for the KNX integration."""
import voluptuous as vol
from xknx.devices.climate import SetpointShiftMode
+from xknx.io import DEFAULT_MCAST_PORT
from homeassistant.const import (
CONF_ADDRESS,
@@ -29,9 +30,9 @@ class ConnectionSchema:
TUNNELING_SCHEMA = vol.Schema(
{
+ vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_KNX_LOCAL_IP): cv.string,
- vol.Optional(CONF_PORT): cv.port,
}
)
@@ -84,27 +85,14 @@ class BinarySensorSchema:
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_SYNC_STATE = CONF_SYNC_STATE
CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state"
- CONF_AUTOMATION = "automation"
- CONF_HOOK = "hook"
- CONF_DEFAULT_HOOK = "on"
- CONF_COUNTER = "counter"
- CONF_DEFAULT_COUNTER = 1
- CONF_ACTION = "action"
+ CONF_CONTEXT_TIMEOUT = "context_timeout"
CONF_RESET_AFTER = "reset_after"
DEFAULT_NAME = "KNX Binary Sensor"
- AUTOMATION_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
- vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
- vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
- }
- )
-
- AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA])
SCHEMA = vol.All(
cv.deprecated("significant_bit"),
+ cv.deprecated("automation"),
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -113,11 +101,13 @@ class BinarySensorSchema:
cv.boolean,
cv.string,
),
- vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean,
+ vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=True): cv.boolean,
+ vol.Optional(CONF_CONTEXT_TIMEOUT, default=1.0): vol.All(
+ vol.Coerce(float), vol.Range(min=0, max=10)
+ ),
vol.Required(CONF_STATE_ADDRESS): cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_RESET_AFTER): cv.positive_int,
- vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
}
),
)
@@ -350,6 +340,7 @@ class WeatherSchema:
CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS = "address_brightness_south"
CONF_KNX_BRIGHTNESS_EAST_ADDRESS = "address_brightness_east"
CONF_KNX_BRIGHTNESS_WEST_ADDRESS = "address_brightness_west"
+ CONF_KNX_BRIGHTNESS_NORTH_ADDRESS = "address_brightness_north"
CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed"
CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm"
CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm"
@@ -374,6 +365,7 @@ class WeatherSchema:
vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string,
vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): cv.string,
vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): cv.string,
+ vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): cv.string,
vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): cv.string,
vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): cv.string,
vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): cv.string,
diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py
index d87119239cf598..fc2cbced8bb6ab 100644
--- a/homeassistant/components/knx/sensor.py
+++ b/homeassistant/components/knx/sensor.py
@@ -1,77 +1,43 @@
"""Support for KNX/IP sensors."""
from xknx.devices import Sensor as XknxSensor
-from homeassistant.core import callback
+from homeassistant.components.sensor import DEVICE_CLASSES
from homeassistant.helpers.entity import Entity
-from . import DATA_KNX
+from .const import DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up sensor(s) for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxSensor):
entities.append(KNXSensor(device))
async_add_entities(entities)
-class KNXSensor(Entity):
+class KNXSensor(KnxEntity, Entity):
"""Representation of a KNX sensor."""
def __init__(self, device: XknxSensor):
"""Initialize of a KNX sensor."""
- self.device = device
-
- @callback
- def async_register_callbacks(self):
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
-
- self.device.register_device_updated_cb(after_update_callback)
-
- async def async_added_to_hass(self):
- """Store register state change callback."""
- self.async_register_callbacks()
-
- async def async_update(self):
- """Update the state from KNX."""
- await self.device.sync()
-
- @property
- def name(self):
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self):
- """No polling needed within KNX."""
- return False
+ super().__init__(device)
@property
def state(self):
"""Return the state of the sensor."""
- return self.device.resolve_state()
+ return self._device.resolve_state()
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
- return self.device.unit_of_measurement()
+ return self._device.unit_of_measurement()
@property
def device_class(self):
"""Return the device class of the sensor."""
- return self.device.ha_device_class()
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
+ device_class = self._device.ha_device_class()
+ if device_class in DEVICE_CLASSES:
+ return device_class
return None
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index c378d1b0ca470b..ae3048e2d23adc 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -2,69 +2,36 @@
from xknx.devices import Switch as XknxSwitch
from homeassistant.components.switch import SwitchEntity
-from homeassistant.core import callback
-from . import DATA_KNX
+from . import DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up switch(es) for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxSwitch):
entities.append(KNXSwitch(device))
async_add_entities(entities)
-class KNXSwitch(SwitchEntity):
+class KNXSwitch(KnxEntity, SwitchEntity):
"""Representation of a KNX switch."""
def __init__(self, device: XknxSwitch):
"""Initialize of KNX switch."""
- self.device = device
-
- @callback
- def async_register_callbacks(self):
- """Register callbacks to update hass after device was changed."""
-
- async def after_update_callback(device):
- """Call after device was updated."""
- self.async_write_ha_state()
-
- self.device.register_device_updated_cb(after_update_callback)
-
- async def async_added_to_hass(self):
- """Store register state change callback."""
- self.async_register_callbacks()
-
- async def async_update(self):
- """Request a state update from KNX bus."""
- await self.device.sync()
-
- @property
- def name(self):
- """Return the name of the KNX device."""
- return self.device.name
-
- @property
- def available(self):
- """Return true if entity is available."""
- return self.hass.data[DATA_KNX].connected
-
- @property
- def should_poll(self):
- """Return the polling state. Not needed within KNX."""
- return False
+ super().__init__(device)
@property
def is_on(self):
"""Return true if device is on."""
- return self.device.state
+ return self._device.state
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
- await self.device.set_on()
+ await self._device.set_on()
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
- await self.device.set_off()
+ await self._device.set_off()
diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py
index 97500ef81947b1..097fa661f4ad0a 100644
--- a/homeassistant/components/knx/weather.py
+++ b/homeassistant/components/knx/weather.py
@@ -1,37 +1,34 @@
"""Support for KNX/IP weather station."""
+
from xknx.devices import Weather as XknxWeather
from homeassistant.components.weather import WeatherEntity
from homeassistant.const import TEMP_CELSIUS
-from .const import DATA_KNX
+from .const import DOMAIN
+from .knx_entity import KnxEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the scenes for KNX platform."""
entities = []
- for device in hass.data[DATA_KNX].xknx.devices:
+ for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxWeather):
entities.append(KNXWeather(device))
async_add_entities(entities)
-class KNXWeather(WeatherEntity):
+class KNXWeather(KnxEntity, WeatherEntity):
"""Representation of a KNX weather device."""
def __init__(self, device: XknxWeather):
"""Initialize of a KNX sensor."""
- self.device = device
-
- @property
- def name(self):
- """Return the name of the weather device."""
- return self.device.name
+ super().__init__(device)
@property
def temperature(self):
"""Return current temperature."""
- return self.device.temperature
+ return self._device.temperature
@property
def temperature_unit(self):
@@ -43,25 +40,27 @@ def pressure(self):
"""Return current air pressure."""
# KNX returns pA - HA requires hPa
return (
- self.device.air_pressure / 100
- if self.device.air_pressure is not None
+ self._device.air_pressure / 100
+ if self._device.air_pressure is not None
else None
)
@property
def condition(self):
"""Return current weather condition."""
- return self.device.ha_current_state().value
+ return self._device.ha_current_state().value
@property
def humidity(self):
"""Return current humidity."""
- return self.device.humidity if self.device.humidity is not None else None
+ return self._device.humidity if self._device.humidity is not None else None
@property
def wind_speed(self):
"""Return current wind speed in km/h."""
# KNX only supports wind speed in m/s
return (
- self.device.wind_speed * 3.6 if self.device.wind_speed is not None else None
+ self._device.wind_speed * 3.6
+ if self._device.wind_speed is not None
+ else None
)
diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py
index c7df170b5c9743..0c4522b5fbd7d1 100644
--- a/homeassistant/components/kodi/browse_media.py
+++ b/homeassistant/components/kodi/browse_media.py
@@ -5,6 +5,7 @@
from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM,
MEDIA_CLASS_ARTIST,
+ MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_MOVIE,
@@ -15,6 +16,7 @@
MEDIA_CLASS_TV_SHOW,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
+ MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_PLAYLIST,
@@ -45,6 +47,7 @@
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
+ MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
}
@@ -64,90 +67,105 @@ async def build_item_response(media_library, payload):
title = None
media = None
- query = {"properties": ["thumbnail"]}
- # pylint: disable=protected-access
+ properties = ["thumbnail"]
if search_type == MEDIA_TYPE_ALBUM:
if search_id:
- query.update({"filter": {"albumid": int(search_id)}})
- query["properties"].extend(
- ["albumid", "artist", "duration", "album", "track"]
- )
- album = await media_library._server.AudioLibrary.GetAlbumDetails(
- {"albumid": int(search_id), "properties": ["thumbnail"]}
+ album = await media_library.get_album_details(
+ album_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
album["albumdetails"].get("thumbnail")
)
title = album["albumdetails"]["label"]
- media = await media_library._server.AudioLibrary.GetSongs(query)
+ media = await media_library.get_songs(
+ album_id=int(search_id),
+ properties=[
+ "albumid",
+ "artist",
+ "duration",
+ "album",
+ "thumbnail",
+ "track",
+ ],
+ )
media = media.get("songs")
else:
- media = await media_library._server.AudioLibrary.GetAlbums(query)
+ media = await media_library.get_albums(properties=properties)
media = media.get("albums")
title = "Albums"
+
elif search_type == MEDIA_TYPE_ARTIST:
if search_id:
- query.update({"filter": {"artistid": int(search_id)}})
- media = await media_library._server.AudioLibrary.GetAlbums(query)
+ media = await media_library.get_albums(
+ artist_id=int(search_id), properties=properties
+ )
media = media.get("albums")
- artist = await media_library._server.AudioLibrary.GetArtistDetails(
- {"artistid": int(search_id), "properties": ["thumbnail"]}
+ artist = await media_library.get_artist_details(
+ artist_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
artist["artistdetails"].get("thumbnail")
)
title = artist["artistdetails"]["label"]
else:
- media = await media_library._server.AudioLibrary.GetArtists(query)
+ media = await media_library.get_artists(properties)
media = media.get("artists")
title = "Artists"
+
elif search_type == "library_music":
library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"}
media = [{"label": name, "type": type_} for type_, name in library.items()]
title = "Music Library"
+
elif search_type == MEDIA_TYPE_MOVIE:
- media = await media_library._server.VideoLibrary.GetMovies(query)
+ media = await media_library.get_movies(properties)
media = media.get("movies")
title = "Movies"
+
elif search_type == MEDIA_TYPE_TVSHOW:
if search_id:
- media = await media_library._server.VideoLibrary.GetSeasons(
- {
- "tvshowid": int(search_id),
- "properties": ["thumbnail", "season", "tvshowid"],
- }
+ media = await media_library.get_seasons(
+ tv_show_id=int(search_id),
+ properties=["thumbnail", "season", "tvshowid"],
)
media = media.get("seasons")
- tvshow = await media_library._server.VideoLibrary.GetTVShowDetails(
- {"tvshowid": int(search_id), "properties": ["thumbnail"]}
+ tvshow = await media_library.get_tv_show_details(
+ tv_show_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
tvshow["tvshowdetails"].get("thumbnail")
)
title = tvshow["tvshowdetails"]["label"]
else:
- media = await media_library._server.VideoLibrary.GetTVShows(query)
+ media = await media_library.get_tv_shows(properties)
media = media.get("tvshows")
title = "TV Shows"
+
elif search_type == MEDIA_TYPE_SEASON:
tv_show_id, season_id = search_id.split("/", 1)
- media = await media_library._server.VideoLibrary.GetEpisodes(
- {
- "tvshowid": int(tv_show_id),
- "season": int(season_id),
- "properties": ["thumbnail", "tvshowid", "seasonid"],
- }
+ media = await media_library.get_episodes(
+ tv_show_id=int(tv_show_id),
+ season_id=int(season_id),
+ properties=["thumbnail", "tvshowid", "seasonid"],
)
media = media.get("episodes")
if media:
- season = await media_library._server.VideoLibrary.GetSeasonDetails(
- {"seasonid": int(media[0]["seasonid"]), "properties": ["thumbnail"]}
+ season = await media_library.get_season_details(
+ season_id=int(media[0]["seasonid"]), properties=properties
)
thumbnail = media_library.thumbnail_url(
season["seasondetails"].get("thumbnail")
)
title = season["seasondetails"]["label"]
+ elif search_type == MEDIA_TYPE_CHANNEL:
+ media = await media_library.get_channels(
+ channel_group_id="alltv",
+ properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
+ )
+ media = media.get("channels")
+ title = "Channels"
+
if media is None:
return None
@@ -227,9 +245,18 @@ def item_payload(item, media_library):
media_content_id = f"{item['tvshowid']}"
can_play = False
can_expand = True
+ elif "channelid" in item:
+ media_content_type = MEDIA_TYPE_CHANNEL
+ media_content_id = f"{item['channelid']}"
+ broadcasting = item.get("broadcastnow")
+ if broadcasting:
+ show = broadcasting.get("title")
+ title = f"{title} - {show}"
+ can_play = True
+ can_expand = False
else:
# this case is for the top folder of each type
- # possible content types: album, artist, movie, library_music, tvshow
+ # possible content types: album, artist, movie, library_music, tvshow, channel
media_class = MEDIA_CLASS_DIRECTORY
media_content_type = item["type"]
media_content_id = ""
@@ -274,6 +301,7 @@ def library_payload(media_library):
"library_music": "Music",
MEDIA_TYPE_MOVIE: "Movies",
MEDIA_TYPE_TVSHOW: "TV shows",
+ MEDIA_TYPE_CHANNEL: "Channels",
}
for item in [{"label": name, "type": type_} for type_, name in library.items()]:
library_info.children.append(
diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py
index f10dcbb2d2806f..c11255aba874e2 100644
--- a/homeassistant/components/kodi/config_flow.py
+++ b/homeassistant/components/kodi/config_flow.py
@@ -116,6 +116,9 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
}
)
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context.update({"title_placeholders": {CONF_NAME: self._name}})
+
try:
await validate_http(self.hass, self._get_data())
await validate_ws(self.hass, self._get_data())
@@ -129,8 +132,6 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- self.context.update({"title_placeholders": {CONF_NAME: self._name}})
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(self, user_input=None):
@@ -201,6 +202,10 @@ async def async_step_ws_port(self, user_input=None):
if user_input is not None:
self._ws_port = user_input.get(CONF_WS_PORT)
+ # optional ints return 0 rather than None when empty
+ if self._ws_port == 0:
+ self._ws_port = None
+
try:
await validate_ws(self.hass, self._get_data())
except WSCannotConnect:
diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json
index b3794d5dfa2218..24d3393d7c37ba 100644
--- a/homeassistant/components/kodi/manifest.json
+++ b/homeassistant/components/kodi/manifest.json
@@ -2,10 +2,15 @@
"domain": "kodi",
"name": "Kodi",
"documentation": "https://www.home-assistant.io/integrations/kodi",
- "requirements": ["pykodi==0.1.2"],
+ "requirements": [
+ "pykodi==0.2.1"
+ ],
"codeowners": [
- "@OnFreund"
+ "@OnFreund",
+ "@cgtobi"
+ ],
+ "zeroconf": [
+ "_xbmc-jsonrpc-h._tcp.local."
],
- "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."],
"config_flow": true
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py
index ce05f3fc732620..68809559cbf40a 100644
--- a/homeassistant/components/kodi/media_player.py
+++ b/homeassistant/components/kodi/media_player.py
@@ -5,6 +5,7 @@
import re
import jsonrpc_base
+from pykodi import CannotConnectError
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@@ -324,11 +325,15 @@ def async_on_volume_changed(self, sender, data):
self._app_properties["muted"] = data["muted"]
self.async_write_ha_state()
- @callback
- def async_on_quit(self, sender, data):
+ async def async_on_quit(self, sender, data):
"""Reset the player state on quit action."""
+ await self._clear_connection()
+
+ async def _clear_connection(self, close=True):
self._reset_state()
- self.hass.async_create_task(self._connection.close())
+ self.async_write_ha_state()
+ if close:
+ await self._connection.close()
@property
def unique_id(self):
@@ -386,14 +391,23 @@ async def _async_ws_connect(self):
try:
await self._connection.connect()
self._on_ws_connected()
- except jsonrpc_base.jsonrpc.TransportError:
- _LOGGER.info("Unable to connect to Kodi via websocket")
+ except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True)
+ await self._clear_connection(False)
+
+ async def _ping(self):
+ try:
+ await self._kodi.ping()
+ except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
+ _LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True)
+ await self._clear_connection()
async def _async_connect_websocket_if_disconnected(self, *_):
"""Reconnect the websocket if it fails."""
if not self._connection.connected:
await self._async_ws_connect()
+ else:
+ await self._ping()
@callback
def _register_ws_callbacks(self):
@@ -464,7 +478,7 @@ def name(self):
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
- return (not self._connection.can_subscribe) or (not self._connection.connected)
+ return not self._connection.can_subscribe
@property
def volume_level(self):
@@ -663,17 +677,10 @@ async def async_play_media(self, media_type, media_id, **kwargs):
elif media_type_lower in [
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_ALBUM,
+ MEDIA_TYPE_TRACK,
]:
await self.async_clear_playlist()
- params = {"playlistid": 0, "item": {f"{media_type}id": int(media_id)}}
- # pylint: disable=protected-access
- await self._kodi._server.Playlist.Add(params)
- await self._kodi.play_playlist(0)
- elif media_type_lower == MEDIA_TYPE_TRACK:
- await self._kodi.clear_playlist()
- params = {"playlistid": 0, "item": {"songid": int(media_id)}}
- # pylint: disable=protected-access
- await self._kodi._server.Playlist.Add(params)
+ await self.async_add_to_playlist(media_type_lower, media_id)
await self._kodi.play_playlist(0)
elif media_type_lower in [
MEDIA_TYPE_MOVIE,
@@ -681,8 +688,7 @@ async def async_play_media(self, media_type, media_id, **kwargs):
MEDIA_TYPE_SEASON,
MEDIA_TYPE_TVSHOW,
]:
- # pylint: disable=protected-access
- await self._kodi._play_item(
+ await self._kodi.play_item(
{MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)}
)
else:
@@ -700,7 +706,7 @@ async def async_call_method(self, method, **kwargs):
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False
try:
- result = self._kodi.call_method(method, **kwargs)
+ result = await self._kodi.call_method(method, **kwargs)
result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]["error"]
@@ -737,6 +743,15 @@ async def async_clear_playlist(self):
"""Clear default playlist (i.e. playlistid=0)."""
await self._kodi.clear_playlist()
+ async def async_add_to_playlist(self, media_type, media_id):
+ """Add media item to default playlist (i.e. playlistid=0)."""
+ if media_type == MEDIA_TYPE_ARTIST:
+ await self._kodi.add_artist_to_playlist(int(media_id))
+ elif media_type == MEDIA_TYPE_ALBUM:
+ await self._kodi.add_album_to_playlist(int(media_id))
+ elif media_type == MEDIA_TYPE_TRACK:
+ await self._kodi.add_song_to_playlist(int(media_id))
+
async def async_add_media_to_playlist(
self, media_type, media_id=None, media_name="ALL", artist_name=""
):
@@ -752,7 +767,7 @@ async def async_add_media_to_playlist(
if media_id is None:
media_id = await self._async_find_song(media_name, artist_name)
if media_id:
- self._kodi.add_song_to_playlist(int(media_id))
+ await self._kodi.add_song_to_playlist(int(media_id))
elif media_type == "ALBUM":
if media_id is None:
@@ -762,7 +777,7 @@ async def async_add_media_to_playlist(
media_id = await self._async_find_album(media_name, artist_name)
if media_id:
- self._kodi.add_album_to_playlist(int(media_id))
+ await self._kodi.add_album_to_playlist(int(media_id))
else:
raise RuntimeError("Unrecognized media type.")
diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json
index 56a637e54ceabd..cf2f265f577783 100644
--- a/homeassistant/components/kodi/strings.json
+++ b/homeassistant/components/kodi/strings.json
@@ -7,7 +7,7 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
- "ssl": "Connect over SSL"
+ "ssl": "[%key:common::config_flow::data::ssl%]"
}
},
"discovery_confirm": {
diff --git a/homeassistant/components/kodi/translations/ca.json b/homeassistant/components/kodi/translations/ca.json
index d9993e01f54e62..09b16682083eac 100644
--- a/homeassistant/components/kodi/translations/ca.json
+++ b/homeassistant/components/kodi/translations/ca.json
@@ -36,7 +36,7 @@
"data": {
"host": "Amfitri\u00f3",
"port": "Port",
- "ssl": "Connexi\u00f3 mitjan\u00e7ant SSL"
+ "ssl": "Utilitza un certificat SSL"
},
"description": "Informaci\u00f3 de connexi\u00f3 de Kodi. Assegura't d'activar \"Permet el control de Kodi via HTTP\" a Sistema/Configuraci\u00f3/Xarxa/Serveis."
},
diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json
new file mode 100644
index 00000000000000..ac98f2016b462c
--- /dev/null
+++ b/homeassistant/components/kodi/translations/de.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "flow_title": "Kodi: {name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ },
+ "host": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/el.json b/homeassistant/components/kodi/translations/el.json
new file mode 100644
index 00000000000000..93b077fbe1d980
--- /dev/null
+++ b/homeassistant/components/kodi/translations/el.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "flow_title": "Kodi: {\u03cc\u03bd\u03bf\u03bc\u03b1}",
+ "step": {
+ "credentials": {
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Kodi. \u0391\u03c5\u03c4\u03ac \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \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."
+ },
+ "discovery_confirm": {
+ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Kodi ('{name}') \u03c3\u03c4\u03bf Home Assistant;",
+ "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 Kodi"
+ },
+ "host": {
+ "data": {
+ "port": "\u0398\u03cd\u03c1\u03b1",
+ "ssl": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 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."
+ },
+ "user": {
+ "data": {
+ "ssl": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 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."
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "description": "\u0397 \u03b8\u03cd\u03c1\u03b1 WebSocket (\u03bc\u03b5\u03c1\u03b9\u03ba\u03ad\u03c2 \u03c6\u03bf\u03c1\u03ad\u03c2 \u03bf\u03bd\u03bf\u03bc\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b8\u03cd\u03c1\u03b1 TCP \u03c3\u03c4\u03bf Kodi). \u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 WebSocket, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03b1 ... \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03bf\u03c5 Kodi\" \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. \u0395\u03ac\u03bd \u03c4\u03bf WebSocket \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf, \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "\u0396\u03b7\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf {entity_name} \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af",
+ "turn_on": "\u0396\u03b7\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf {entity_name} \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/en.json b/homeassistant/components/kodi/translations/en.json
index 4a9a48ea9ca003..0f703057dcfaa5 100644
--- a/homeassistant/components/kodi/translations/en.json
+++ b/homeassistant/components/kodi/translations/en.json
@@ -36,7 +36,7 @@
"data": {
"host": "Host",
"port": "Port",
- "ssl": "Connect over SSL"
+ "ssl": "Uses an SSL certificate"
},
"description": "Kodi connection information. Please make sure to enable \"Allow control of Kodi via HTTP\" in System/Settings/Network/Services."
},
diff --git a/homeassistant/components/kodi/translations/et.json b/homeassistant/components/kodi/translations/et.json
new file mode 100644
index 00000000000000..9d7c8e2a02895f
--- /dev/null
+++ b/homeassistant/components/kodi/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} paluti v\u00e4lja l\u00fclitada",
+ "turn_on": "{entity_name} paluti sisse l\u00fclitada"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/kodi/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/it.json b/homeassistant/components/kodi/translations/it.json
index a48d8ca9bb0041..565aaa9d39ac51 100644
--- a/homeassistant/components/kodi/translations/it.json
+++ b/homeassistant/components/kodi/translations/it.json
@@ -36,7 +36,7 @@
"data": {
"host": "Host",
"port": "Porta",
- "ssl": "Connettiti tramite SSL"
+ "ssl": "Utilizza un certificato SSL"
},
"description": "Informazioni sulla connessione Kodi. Assicurati di abilitare \"Consenti il controllo di Kodi tramite HTTP\" in Sistema/Impostazioni/Rete/Servizi."
},
diff --git a/homeassistant/components/kodi/translations/ko.json b/homeassistant/components/kodi/translations/ko.json
new file mode 100644
index 00000000000000..6dc6b8bf87ad0e
--- /dev/null
+++ b/homeassistant/components/kodi/translations/ko.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "step": {
+ "credentials": {
+ "description": "Kodi \uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624. \uc774\ub7ec\ud55c \ub0b4\uc6a9\uc740 \uc2dc\uc2a4\ud15c/\uc124\uc815/\ub124\ud2b8\uc6cc\ud06c/\uc11c\ube44\uc2a4\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "discovery_confirm": {
+ "description": "Kodi (` {name} `)\ub97c Home Assistant\uc5d0 \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Kodi \ubc1c\uacac"
+ },
+ "host": {
+ "data": {
+ "ssl": "SSL\uc744 \ud1b5\ud574 \uc5f0\uacb0"
+ },
+ "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624."
+ },
+ "user": {
+ "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624."
+ },
+ "ws_port": {
+ "description": "WebSocket \ud3ec\ud2b8 (Kodi\uc5d0\uc11c TCP \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568). WebSocket\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"\ud504\ub85c\uadf8\ub7a8\uc774 Kodi\ub97c \uc81c\uc5b4\ud558\ub3c4\ub85d \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c\ud569\ub2c8\ub2e4. WebSocket\uc774 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc \ub461\ub2c8\ub2e4."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/lb.json b/homeassistant/components/kodi/translations/lb.json
index 872615f5bea24a..0fde1e6ffdf819 100644
--- a/homeassistant/components/kodi/translations/lb.json
+++ b/homeassistant/components/kodi/translations/lb.json
@@ -43,8 +43,15 @@
"ws_port": {
"data": {
"ws_port": "Port"
- }
+ },
+ "description": "De Websocket Port (heiansdo TCP port am Kodi genannt). Fir sech k\u00ebnnen iwwer Websocket ze verbannen muss du \"Allow programs ... to control Kodi\" an de System/Settings/Network/Services aktiv\u00e9ieren. Falls Websocket net aktiv\u00e9iert ass, l\u00e4sch de Port a loss eidel."
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} soll ugeschalt ginn",
+ "turn_on": "{entity_name} soll ugeschalt ginn"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json
new file mode 100644
index 00000000000000..235d5a50be6365
--- /dev/null
+++ b/homeassistant/components/kodi/translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "credentials": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Poort",
+ "ssl": "Maak verbinding via SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/no.json b/homeassistant/components/kodi/translations/no.json
index 1c8bc3d6a5310b..fc4ae7ff736e95 100644
--- a/homeassistant/components/kodi/translations/no.json
+++ b/homeassistant/components/kodi/translations/no.json
@@ -36,7 +36,7 @@
"data": {
"host": "Vert",
"port": "Port",
- "ssl": "Koble til via SSL"
+ "ssl": "Bruker et SSL-sertifikat"
},
"description": "Kodi-tilkoblingsinformasjon. Vennligst s\u00f8rg for \u00e5 aktivere \"Tillat kontroll av Kodi via HTTP\" i System / Innstillinger / Nettverk / Tjenester."
},
diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json
index 3e71fd0df8b62c..ac84350cf93988 100644
--- a/homeassistant/components/kodi/translations/pl.json
+++ b/homeassistant/components/kodi/translations/pl.json
@@ -1,40 +1,56 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
+ "flow_title": "Kodi: {name}",
"step": {
"credentials": {
"data": {
"password": "Has\u0142o",
"username": "Nazwa u\u017cytkownika"
- }
+ },
+ "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o Kodi. Mo\u017cna je znale\u017a\u0107 w System/Ustawienia/Sie\u0107/Us\u0142ugi."
+ },
+ "discovery_confirm": {
+ "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistant?",
+ "title": "Wykryto urz\u0105dzenia Kodi"
},
"host": {
"data": {
"host": "Nazwa hosta lub adres IP",
- "port": "Port"
- }
+ "port": "Port",
+ "ssl": "Po\u0142\u0105cz przez SSL"
+ },
+ "description": "Informacje o po\u0142\u0105czeniu Kodi. Upewnij si\u0119, \u017ce w\u0142\u0105czy\u0142e\u015b \"Zezwalaj na zdalne sterowanie przez HTTP\" w System/Ustawienia/Sieci/Us\u0142ugi."
},
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"port": "Port"
- }
+ },
+ "description": "Informacje o po\u0142\u0105czeniu Kodi. Upewnij si\u0119, \u017ce w\u0142\u0105czy\u0142e\u015b \"Zezwalaj na zdalne sterowanie przez HTTP\" w System/Ustawienia/Sieci/Us\u0142ugi."
},
"ws_port": {
"data": {
"ws_port": "Port"
- }
+ },
+ "description": "Port WebSocket (czasami nazywany portem TCP w Kodi). Aby po\u0142\u0105czy\u0107 si\u0119 przez WebSocket, musisz w\u0142\u0105czy\u0107 opcj\u0119 \u201eZezwalaj ... programom na kontrolowanie Kodi\u201d w System / Ustawienia / Sie\u0107 / Us\u0142ugi. Je\u015bli us\u0142uga WebSocket nie jest w\u0142\u0105czona, usu\u0144 port i pozostaw pusty."
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} zosta\u0142 poproszony o wy\u0142\u0105czenie",
+ "turn_on": "{entity_name} zosta\u0142 poproszony o w\u0142\u0105czenie"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json
index 8702c016871ff1..3e6a898bf43cb4 100644
--- a/homeassistant/components/kodi/translations/ru.json
+++ b/homeassistant/components/kodi/translations/ru.json
@@ -36,7 +36,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e SSL"
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e HTTP\" \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"."
},
diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json
index b00f6245366375..c36fe96740b9d8 100644
--- a/homeassistant/components/kodi/translations/zh-Hant.json
+++ b/homeassistant/components/kodi/translations/zh-Hant.json
@@ -36,7 +36,7 @@
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u901a\u904e SSL \u9023\u7dda"
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49"
},
"description": "Kodi \u9023\u7dda\u8cc7\u8a0a\uff0c\u8acb\u78ba\u5b9a\u5df2\u65bc\u300c\u7cfb\u7d71/\u8a2d\u5b9a/\u7db2\u8def/\u670d\u52d9\u300d\u4e2d\u958b\u555f \"\u5141\u8a31\u900f\u904e HTTP \u63a7\u5236 Kodi\"\u3002"
},
diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json
index 281de6032eeb9f..4fd62dee8e7c9d 100644
--- a/homeassistant/components/konnected/strings.json
+++ b/homeassistant/components/konnected/strings.json
@@ -18,12 +18,12 @@
}
},
"error": {
- "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "unknown": "Unknown error occurred",
- "already_configured": "Device is already configured",
- "already_in_progress": "Config flow for device is already in progress.",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"not_konn_panel": "Not a recognized Konnected.io device"
}
},
@@ -62,7 +62,7 @@
"description": "{zone} options",
"data": {
"type": "Binary Sensor Type",
- "name": "Name (optional)",
+ "name": "[%key:common::config_flow::data::name%] (optional)",
"inverse": "Invert the open/close state"
}
},
@@ -71,7 +71,7 @@
"description": "{zone} options",
"data": {
"type": "Sensor Type",
- "name": "Name (optional)",
+ "name": "[%key:common::config_flow::data::name%] (optional)",
"poll_interval": "Poll Interval (minutes) (optional)"
}
},
@@ -79,7 +79,7 @@
"title": "Configure Switchable Output",
"description": "{zone} options: state {state}",
"data": {
- "name": "Name (optional)",
+ "name": "[%key:common::config_flow::data::name%] (optional)",
"activation": "Output when on",
"momentary": "Pulse duration (ms) (optional)",
"pause": "Pause between pulses (ms) (optional)",
diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json
index 8938db2621b42e..68a62bd7d32673 100644
--- a/homeassistant/components/konnected/translations/ca.json
+++ b/homeassistant/components/konnected/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io",
"unknown": "S'ha produ\u00eft un error desconegut"
},
diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json
index 3b9b422c903ae4..88a109e13011a7 100644
--- a/homeassistant/components/konnected/translations/en.json
+++ b/homeassistant/components/konnected/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "Config flow for device is already in progress.",
+ "already_in_progress": "Configuration flow is already in progress",
"not_konn_panel": "Not a recognized Konnected.io device",
"unknown": "Unknown error occurred"
},
diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json
index be48b5e15c2807..3c020967c8d5d3 100644
--- a/homeassistant/components/konnected/translations/fr.json
+++ b/homeassistant/components/konnected/translations/fr.json
@@ -32,6 +32,7 @@
"not_konn_panel": "Non reconnu comme appareil Konnected.io"
},
"error": {
+ "bad_host": "URL de substitution de l'h\u00f4te de l'API non valide",
"one": "Vide",
"other": "Vide"
},
@@ -65,6 +66,7 @@
"7": "Zone 7",
"out": "OUT"
},
+ "description": "D\u00e9couverte d\u2019un {model} \u00e0 {host}. S\u00e9lectionnez la configuration de base de chaque E/S ci-dessous - en fonction de l\u2019E/S, il peut permettre des capteurs binaires (contacts ouverts/proches), des capteurs num\u00e9riques (dht et ds18b20) ou des sorties commutables. Vous pourrez configurer des options d\u00e9taill\u00e9es dans les \u00e9tapes suivantes.",
"title": "Configurer les E/S"
},
"options_io_ext": {
@@ -83,8 +85,10 @@
},
"options_misc": {
"data": {
+ "api_host": "Remplacer l'URL de l'h\u00f4te de l'API (facultatif)",
"blink": "Voyant du panneau clignotant lors de l'envoi d'un changement d'\u00e9tat",
- "discovery": "R\u00e9pondre aux demandes de d\u00e9couverte sur votre r\u00e9seau"
+ "discovery": "R\u00e9pondre aux demandes de d\u00e9couverte sur votre r\u00e9seau",
+ "override_api_host": "Remplacer l'URL par d\u00e9faut du panneau h\u00f4te de l'API Home Assistant"
},
"description": "Veuillez s\u00e9lectionner le comportement souhat\u00e9 de votre panneau",
"title": "Configurer divers"
diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json
index ff8b6da9e3daab..f0820198e7e1f4 100644
--- a/homeassistant/components/konnected/translations/it.json
+++ b/homeassistant/components/konnected/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto",
"unknown": "Si \u00e8 verificato un errore sconosciuto"
},
diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json
index dcb5f1ed6c4182..8e9eba0d134c19 100644
--- a/homeassistant/components/konnected/translations/nl.json
+++ b/homeassistant/components/konnected/translations/nl.json
@@ -2,8 +2,13 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom voor het apparaat wordt al uitgevoerd.",
+ "not_konn_panel": "Geen herkend Konnected.io apparaat",
"unknown": "Onbekende fout opgetreden"
},
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken met een Konnected Panel op {host} : {port}"
+ },
"step": {
"confirm": {
"title": "Konnected Apparaat Klaar"
diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json
index af39b9750a421a..c1d18939741a1d 100644
--- a/homeassistant/components/konnected/translations/no.json
+++ b/homeassistant/components/konnected/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"not_konn_panel": "Ikke en anerkjent Konnected.io-enhet",
"unknown": "Ukjent feil oppstod"
},
diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json
index 1f9b60bbae3d78..95b3a9f5d5e50e 100644
--- a/homeassistant/components/konnected/translations/pl.json
+++ b/homeassistant/components/konnected/translations/pl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
"not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}"
diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json
index 433d6bad4c991b..9963fb50f02ef0 100644
--- a/homeassistant/components/konnected/translations/ru.json
+++ b/homeassistant/components/konnected/translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e.",
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json
index c2c63e20c3857a..8ee25150c9c3cc 100644
--- a/homeassistant/components/konnected/translations/zh-Hant.json
+++ b/homeassistant/components/konnected/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099",
"unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json
index 74bfe10555551d..6eeb4a69b26b90 100644
--- a/homeassistant/components/lcn/manifest.json
+++ b/homeassistant/components/lcn/manifest.json
@@ -2,6 +2,6 @@
"domain": "lcn",
"name": "LCN",
"documentation": "https://www.home-assistant.io/integrations/lcn",
- "requirements": ["pypck==0.6.4"],
+ "requirements": ["pypck==0.7.2"],
"codeowners": ["@alengwenus"]
}
diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json
new file mode 100644
index 00000000000000..6ea3ab1c89e3fe
--- /dev/null
+++ b/homeassistant/components/life360/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unexpected": "Ootamatu t\u00f5rge Life360 serveriga suhtlemisel"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json
index 19a6c6d8828a9a..b0a5785a320a73 100644
--- a/homeassistant/components/life360/translations/pl.json
+++ b/homeassistant/components/life360/translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
- "user_already_configured": "Konto jest ju\u017c skonfigurowane."
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane"
},
"create_entry": {
"default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})."
@@ -11,7 +11,7 @@
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
"invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika",
"unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360",
- "user_already_configured": "Konto jest ju\u017c skonfigurowane."
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane"
},
"step": {
"user": {
diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json
index c35dcf3eb26e0a..ebb8b39a8bc68b 100644
--- a/homeassistant/components/lifx/strings.json
+++ b/homeassistant/components/lifx/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of LIFX is possible.",
- "no_devices_found": "No LIFX devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json
index edc525a92cb911..28c25cde70ac5c 100644
--- a/homeassistant/components/lifx/translations/ca.json
+++ b/homeassistant/components/lifx/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius LIFX a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de LIFX."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json
index ab4d5458d824d3..154101995ac9b4 100644
--- a/homeassistant/components/lifx/translations/en.json
+++ b/homeassistant/components/lifx/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No LIFX devices found on the network.",
- "single_instance_allowed": "Only a single configuration of LIFX is possible."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json
index 40b4c98f4907ea..be167ec99948a6 100644
--- a/homeassistant/components/lifx/translations/it.json
+++ b/homeassistant/components/lifx/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo LIFX trovato in rete.",
- "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di LIFX."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json
index 708efba9cc71a8..4771b4c05d7c6c 100644
--- a/homeassistant/components/lifx/translations/no.json
+++ b/homeassistant/components/lifx/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen LIFX-enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Kun en konfigurasjon av LIFX er mulig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json
index 9ffd9a95270769..0d50dec498b70a 100644
--- a/homeassistant/components/lifx/translations/ru.json
+++ b/homeassistant/components/lifx/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 LIFX \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json
index bc20e93c596628..ed704711a661e1 100644
--- a/homeassistant/components/lifx/translations/zh-Hant.json
+++ b/homeassistant/components/lifx/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py
index 75ab46567943de..cf39d70d89abfa 100644
--- a/homeassistant/components/lifx_cloud/scene.py
+++ b/homeassistant/components/lifx_cloud/scene.py
@@ -9,7 +9,13 @@
import voluptuous as vol
from homeassistant.components.scene import Scene
-from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN, HTTP_OK
+from homeassistant.const import (
+ CONF_PLATFORM,
+ CONF_TIMEOUT,
+ CONF_TOKEN,
+ HTTP_OK,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -50,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
devices = [LifxCloudScene(hass, headers, timeout, scene) for scene in data]
async_add_entities(devices)
return True
- if status == 401:
+ if status == HTTP_UNAUTHORIZED:
_LOGGER.error("Unauthorized (bad token?) on %s", url)
return False
diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/light/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/light/translations/et.json b/homeassistant/components/light/translations/et.json
index 4eef7a8526746a..f137faa0bf7757 100644
--- a/homeassistant/components/light/translations/et.json
+++ b/homeassistant/components/light/translations/et.json
@@ -1,4 +1,22 @@
{
+ "device_automation": {
+ "action_type": {
+ "brightness_decrease": "V\u00e4henda {entity_name} heledust",
+ "brightness_increase": "Suurenda{entity_name} heledust",
+ "flash": "Vilguta {entity_name}",
+ "toggle": "Muuda {entity_name} olekut",
+ "turn_off": "L\u00fclita {entity_name} v\u00e4lja",
+ "turn_on": "L\u00fclita {entity_name} sisse"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
diff --git a/homeassistant/components/light/translations/uk.json b/homeassistant/components/light/translations/uk.json
index 06c880fff77add..67685889c5489d 100644
--- a/homeassistant/components/light/translations/uk.json
+++ b/homeassistant/components/light/translations/uk.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py
index c4a14210d32b2e..bb81a022891139 100644
--- a/homeassistant/components/linode/binary_sensor.py
+++ b/homeassistant/components/linode/binary_sensor.py
@@ -3,7 +3,11 @@
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOVING,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
import homeassistant.helpers.config_validation as cv
from . import (
@@ -22,7 +26,6 @@
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Node"
-DEFAULT_DEVICE_CLASS = "moving"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])}
)
@@ -69,7 +72,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this sensor."""
- return DEFAULT_DEVICE_CLASS
+ return DEVICE_CLASS_MOVING
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json
index 2c571be1a0ae96..1859223e6570ae 100644
--- a/homeassistant/components/local_ip/strings.json
+++ b/homeassistant/components/local_ip/strings.json
@@ -10,7 +10,7 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Local IP is allowed."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/local_ip/translations/ca.json b/homeassistant/components/local_ip/translations/ca.json
index e0df6934361508..7be2a2d70d6cef 100644
--- a/homeassistant/components/local_ip/translations/ca.json
+++ b/homeassistant/components/local_ip/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Nom\u00e9s \u00e9s possible configurar una sola IP local."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"user": {
diff --git a/homeassistant/components/local_ip/translations/en.json b/homeassistant/components/local_ip/translations/en.json
index 5a4077352e701d..7f823968f9c0af 100644
--- a/homeassistant/components/local_ip/translations/en.json
+++ b/homeassistant/components/local_ip/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Only a single configuration of Local IP is allowed."
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/local_ip/translations/et.json b/homeassistant/components/local_ip/translations/et.json
new file mode 100644
index 00000000000000..70493aee468f05
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Anduri nimi"
+ },
+ "title": "Kohalik IP-aadress"
+ }
+ }
+ },
+ "title": "Kohalik IP-aadress"
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/translations/it.json b/homeassistant/components/local_ip/translations/it.json
index 3e1592546003b7..9173584c9f8469 100644
--- a/homeassistant/components/local_ip/translations/it.json
+++ b/homeassistant/components/local_ip/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u00c8 consentita una sola configurazione dell'IP locale."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"user": {
diff --git a/homeassistant/components/locative/strings.json b/homeassistant/components/locative/strings.json
index 53a0c160e99a29..3a5821f40b1174 100644
--- a/homeassistant/components/locative/strings.json
+++ b/homeassistant/components/locative/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency."
},
"create_entry": {
diff --git a/homeassistant/components/locative/translations/ca.json b/homeassistant/components/locative/translations/ca.json
index 61c6a6c48afa18..89dcafd65d5bfd 100644
--- a/homeassistant/components/locative/translations/ca.json
+++ b/homeassistant/components/locative/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Geofency.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar ubicacions a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de l'aplicaci\u00f3 Locative.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/locative/translations/el.json b/homeassistant/components/locative/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/locative/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/en.json b/homeassistant/components/locative/translations/en.json
index 750797e535bf19..59ff777603e3d7 100644
--- a/homeassistant/components/locative/translations/en.json
+++ b/homeassistant/components/locative/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/locative/translations/es.json b/homeassistant/components/locative/translations/es.json
index 53188cd7be2325..d095c46744d6da 100644
--- a/homeassistant/components/locative/translations/es.json
+++ b/homeassistant/components/locative/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de Geofency.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar ubicaciones a Home Assistant, es necesario configurar la caracter\u00edstica webhook en la app de Locative.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nRevisa [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/locative/translations/et.json b/homeassistant/components/locative/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/locative/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/fr.json b/homeassistant/components/locative/translations/fr.json
index 4e65415b1b3ef3..1327b2d0f8bff1 100644
--- a/homeassistant/components/locative/translations/fr.json
+++ b/homeassistant/components/locative/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Geofency.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des localisations \u00e0 Home Assistant, vous devez configurer la fonctionnalit\u00e9 Webhook dans l'application Locative. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
diff --git a/homeassistant/components/locative/translations/it.json b/homeassistant/components/locative/translations/it.json
index 210fbfe4c2848f..37e47c11aefa44 100644
--- a/homeassistant/components/locative/translations/it.json
+++ b/homeassistant/components/locative/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli."
diff --git a/homeassistant/components/locative/translations/lb.json b/homeassistant/components/locative/translations/lb.json
index 42086f5cfe5822..06d303ff0b1a8d 100644
--- a/homeassistant/components/locative/translations/lb.json
+++ b/homeassistant/components/locative/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Geofency Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Plazen un Home Assistant ze sch\u00e9cken, muss den Webhook Feature an der Locative App ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/locative/translations/no.json b/homeassistant/components/locative/translations/no.json
index 46f9f413236075..fdb42ff7e12528 100644
--- a/homeassistant/components/locative/translations/no.json
+++ b/homeassistant/components/locative/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 kunne sende steder til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Locative. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer."
diff --git a/homeassistant/components/locative/translations/ru.json b/homeassistant/components/locative/translations/ru.json
index 591f2f423024d2..0981c9f2f1c683 100644
--- a/homeassistant/components/locative/translations/ru.json
+++ b/homeassistant/components/locative/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Locative.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\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]({docs_url}) \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."
diff --git a/homeassistant/components/locative/translations/zh-Hant.json b/homeassistant/components/locative/translations/zh-Hant.json
index 3b0089ed220bcd..f84a40c1152e14 100644
--- a/homeassistant/components/locative/translations/zh-Hant.json
+++ b/homeassistant/components/locative/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u5ea7\u6a19\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py
new file mode 100644
index 00000000000000..d64b2172750638
--- /dev/null
+++ b/homeassistant/components/lock/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_LOCKED}, STATE_UNLOCKED)
diff --git a/homeassistant/components/lock/translations/et.json b/homeassistant/components/lock/translations/et.json
index 448d1d4531ad46..1ebf7e1e6dd751 100644
--- a/homeassistant/components/lock/translations/et.json
+++ b/homeassistant/components/lock/translations/et.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "lock": "Lukusta {entity_name}",
+ "open": "Ava {entity_name}",
+ "unlock": "Tee {entity_name} lahti"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} on lukus",
+ "is_unlocked": "{entity_name} on lukustamata"
+ },
+ "trigger_type": {
+ "locked": "{entity_name} on lukus",
+ "unlocked": "{entity_name} on lukustamata"
+ }
+ },
"state": {
"_": {
"locked": "Lukus",
diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py
index 0c7786de90bf2a..9eb3c80435cb29 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -7,25 +7,24 @@
import sqlalchemy
from sqlalchemy.orm import aliased
+from sqlalchemy.sql.expression import literal
import voluptuous as vol
-from homeassistant.components import sun
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.models import (
Events,
States,
- process_timestamp,
process_timestamp_to_utc_isoformat,
)
from homeassistant.components.recorder.util import session_scope
from homeassistant.components.script import EVENT_SCRIPT_STARTED
from homeassistant.const import (
- ATTR_DEVICE_CLASS,
ATTR_DOMAIN,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
ATTR_NAME,
ATTR_SERVICE,
EVENT_CALL_SERVICE,
@@ -34,16 +33,8 @@
EVENT_LOGBOOK_ENTRY,
EVENT_STATE_CHANGED,
HTTP_BAD_REQUEST,
- STATE_NOT_HOME,
- STATE_OFF,
- STATE_ON,
-)
-from homeassistant.core import (
- DOMAIN as HA_DOMAIN,
- callback,
- split_entity_id,
- valid_entity_id,
)
+from homeassistant.core import DOMAIN as HA_DOMAIN, callback, split_entity_id
from homeassistant.exceptions import InvalidEntityFormatError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import (
@@ -60,6 +51,7 @@
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": "([^"]+)"')
_LOGGER = logging.getLogger(__name__)
@@ -76,6 +68,8 @@
EMPTY_JSON_OBJECT = "{}"
UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":'
+HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}."
+
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA
)
@@ -85,13 +79,25 @@
EVENT_HOMEASSISTANT_STOP,
]
-ALL_EVENT_TYPES = [
- EVENT_STATE_CHANGED,
+ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = [
EVENT_LOGBOOK_ENTRY,
EVENT_CALL_SERVICE,
*HOMEASSISTANT_EVENTS,
]
+ALL_EVENT_TYPES = [
+ EVENT_STATE_CHANGED,
+ *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED,
+]
+
+EVENT_COLUMNS = [
+ Events.event_type,
+ Events.event_data,
+ Events.time_fired,
+ Events.context_id,
+ Events.context_user_id,
+]
+
SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED]
LOG_MESSAGE_SCHEMA = vol.Schema(
@@ -206,7 +212,15 @@ async def get(self, request, datetime=None):
else:
period = int(period)
- entity_id = request.query.get("entity")
+ entity_ids = request.query.get("entity")
+ if entity_ids:
+ try:
+ entity_ids = cv.entity_ids(entity_ids)
+ except vol.Invalid:
+ raise InvalidEntityFormatError(
+ f"Invalid entity id(s) encountered: {entity_ids}. "
+ "Format should be ."
+ ) from vol.Invalid
end_time = request.query.get("end_time")
if end_time is None:
@@ -229,7 +243,7 @@ def json_events():
hass,
start_day,
end_day,
- entity_id,
+ entity_ids,
self.filters,
self.entities_filter,
entity_matches_only,
@@ -246,6 +260,7 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
- if 2+ sensor updates in GROUP_BY_MINUTES, show last
- if Home Assistant stop and start happen in same minute call it restarted
"""
+ external_events = hass.data.get(DOMAIN, {})
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
@@ -280,27 +295,7 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
start_stop_events[event.time_fired_minute] = 2
# Yield entries
- external_events = hass.data.get(DOMAIN, {})
for event in events_batch:
- if event.event_type in external_events:
- domain, describe_event = external_events[event.event_type]
- data = describe_event(event)
- data["when"] = event.time_fired_isoformat
- data["domain"] = domain
- if event.context_user_id:
- data["context_user_id"] = event.context_user_id
- context_event = context_lookup.get(event.context_id)
- if context_event:
- _augment_data_with_context(
- data,
- data.get(ATTR_ENTITY_ID),
- event,
- context_event,
- entity_attr_cache,
- external_events,
- )
- yield data
-
if event.event_type == EVENT_STATE_CHANGED:
entity_id = event.entity_id
domain = event.domain
@@ -317,13 +312,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
"name": _entity_name_from_event(
entity_id, event, entity_attr_cache
),
- "message": _entry_message_from_event(
- entity_id, domain, event, entity_attr_cache
- ),
- "domain": domain,
+ "state": event.state,
"entity_id": entity_id,
}
+ icon = event.attributes_icon
+ if icon:
+ data["icon"] = icon
+
if event.context_user_id:
data["context_user_id"] = event.context_user_id
@@ -340,6 +336,25 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
yield data
+ elif event.event_type in external_events:
+ domain, describe_event = external_events[event.event_type]
+ data = describe_event(event)
+ data["when"] = event.time_fired_isoformat
+ data["domain"] = domain
+ if event.context_user_id:
+ data["context_user_id"] = event.context_user_id
+ context_event = context_lookup.get(event.context_id)
+ if context_event:
+ _augment_data_with_context(
+ data,
+ data.get(ATTR_ENTITY_ID),
+ event,
+ context_event,
+ entity_attr_cache,
+ external_events,
+ )
+ yield data
+
elif event.event_type == EVENT_HOMEASSISTANT_START:
if start_stop_events.get(event.time_fired_minute) == 2:
continue
@@ -381,6 +396,7 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
"domain": domain,
"entity_id": entity_id,
}
+
if event.context_user_id:
data["context_user_id"] = event.context_user_id
@@ -402,231 +418,185 @@ def _get_events(
hass,
start_day,
end_day,
- entity_id=None,
+ entity_ids=None,
filters=None,
entities_filter=None,
entity_matches_only=False,
):
"""Get events for a period of time."""
+
entity_attr_cache = EntityAttributeCache(hass)
context_lookup = {None: None}
- entity_id_lower = None
- apply_sql_entities_filter = True
def yield_events(query):
"""Yield Events that are not filtered away."""
for row in query.yield_per(1000):
event = LazyEventPartialState(row)
context_lookup.setdefault(event.context_id, event)
- if _keep_event(hass, event, entities_filter):
+ if event.event_type == EVENT_CALL_SERVICE:
+ continue
+ if event.event_type == EVENT_STATE_CHANGED or _keep_event(
+ hass, event, entities_filter
+ ):
yield event
- if entity_id is not None:
- entity_id_lower = entity_id.lower()
- if not valid_entity_id(entity_id_lower):
- raise InvalidEntityFormatError(
- f"Invalid entity id encountered: {entity_id_lower}. "
- "Format should be ."
- )
- entities_filter = generate_filter([], [entity_id_lower], [], [])
- apply_sql_entities_filter = False
+ if entity_ids is not None:
+ entities_filter = generate_filter([], entity_ids, [], [])
with session_scope(hass=hass) as session:
old_state = aliased(States, name="old_state")
- query = (
- session.query(
- Events.event_type,
- Events.event_data,
- Events.time_fired,
- Events.context_id,
- Events.context_user_id,
- States.state,
- States.entity_id,
- States.domain,
- States.attributes,
- )
- .order_by(Events.time_fired)
- .outerjoin(States, (Events.event_id == States.event_id))
- .outerjoin(old_state, (States.old_state_id == old_state.state_id))
- # The below filter, removes state change events that do not have
- # and old_state, new_state, or the old and
- # new state.
- #
- .filter(
- (Events.event_type != EVENT_STATE_CHANGED)
- | (
- (States.state_id.isnot(None))
- & (old_state.state_id.isnot(None))
- & (States.state.isnot(None))
- & (States.state != old_state.state)
- )
- )
- #
- # Prefilter out continuous domains that have
- # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.
- #
- .filter(
- (Events.event_type != EVENT_STATE_CHANGED)
- | sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS))
- | sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON))
+ if entity_ids is not None:
+ query = _generate_events_query_without_states(session)
+ query = _apply_event_time_filter(query, start_day, end_day)
+ query = _apply_event_types_filter(
+ hass, query, ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED
)
- .filter(
- Events.event_type.in_(ALL_EVENT_TYPES + list(hass.data.get(DOMAIN, {})))
- )
- .filter((Events.time_fired > start_day) & (Events.time_fired < end_day))
- )
-
- if entity_id_lower is not None:
if entity_matches_only:
# When entity_matches_only is provided, contexts and events that do not
- # contain the entity_id are not included in the logbook response.
- entity_id_json = ENTITY_ID_JSON_TEMPLATE.format(entity_id_lower)
- query = query.filter(
- (
- (States.last_updated == States.last_changed)
- & (States.entity_id == entity_id_lower)
- )
- | (
- States.state_id.is_(None)
- & Events.event_data.contains(entity_id_json)
- )
- )
- else:
- query = query.filter(
- (
- (States.last_updated == States.last_changed)
- & (States.entity_id == entity_id_lower)
- )
- | (States.state_id.is_(None))
+ # contain the entity_ids are not included in the logbook response.
+ query = _apply_event_entity_id_matchers(query, entity_ids)
+
+ query = query.union_all(
+ _generate_states_query(
+ session, start_day, end_day, old_state, entity_ids
)
+ )
else:
- query = query.filter(
+ query = _generate_events_query(session)
+ query = _apply_event_time_filter(query, start_day, end_day)
+ query = _apply_events_types_and_states_filter(
+ hass, query, old_state
+ ).filter(
(States.last_updated == States.last_changed)
- | (States.state_id.is_(None))
+ | (Events.event_type != EVENT_STATE_CHANGED)
)
-
- if apply_sql_entities_filter and filters:
- entity_filter = filters.entity_filter()
- if entity_filter is not None:
+ if filters:
query = query.filter(
- entity_filter | (Events.event_type != EVENT_STATE_CHANGED)
+ filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED)
)
+ query = query.order_by(Events.time_fired)
+
return list(
humanify(hass, yield_events(query), entity_attr_cache, context_lookup)
)
+def _generate_events_query(session):
+ return session.query(
+ *EVENT_COLUMNS,
+ States.state,
+ States.entity_id,
+ States.domain,
+ States.attributes,
+ )
+
+
+def _generate_events_query_without_states(session):
+ return session.query(
+ *EVENT_COLUMNS,
+ literal(None).label("state"),
+ literal(None).label("entity_id"),
+ literal(None).label("domain"),
+ literal(None).label("attributes"),
+ )
+
+
+def _generate_states_query(session, start_day, end_day, old_state, entity_ids):
+ 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((States.last_updated > start_day) & (States.last_updated < end_day))
+ .filter(
+ (States.last_updated == States.last_changed)
+ & States.entity_id.in_(entity_ids)
+ )
+ )
+
+
+def _apply_events_types_and_states_filter(hass, query, old_state):
+ events_query = (
+ query.outerjoin(States, (Events.event_id == States.event_id))
+ .outerjoin(old_state, (States.old_state_id == old_state.state_id))
+ .filter(
+ (Events.event_type != EVENT_STATE_CHANGED)
+ | _missing_state_matcher(old_state)
+ )
+ .filter(
+ (Events.event_type != EVENT_STATE_CHANGED) | _continuous_entity_matcher()
+ )
+ )
+ return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES)
+
+
+def _missing_state_matcher(old_state):
+ # 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)
+ return sqlalchemy.and_(
+ old_state.state_id.isnot(None),
+ (States.state != old_state.state),
+ States.state.isnot(None),
+ )
+
+
+def _continuous_entity_matcher():
+ #
+ # Prefilter out continuous domains that have
+ # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.
+ #
+ return sqlalchemy.or_(
+ sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)),
+ sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)),
+ )
+
+
+def _apply_event_time_filter(events_query, start_day, end_day):
+ return events_query.filter(
+ (Events.time_fired > start_day) & (Events.time_fired < end_day)
+ )
+
+
+def _apply_event_types_filter(hass, query, event_types):
+ return query.filter(
+ Events.event_type.in_(event_types + list(hass.data.get(DOMAIN, {})))
+ )
+
+
+def _apply_event_entity_id_matchers(events_query, entity_ids):
+ return events_query.filter(
+ sqlalchemy.or_(
+ *[
+ Events.event_data.contains(ENTITY_ID_JSON_TEMPLATE.format(entity_id))
+ for entity_id in entity_ids
+ ]
+ )
+ )
+
+
def _keep_event(hass, event, entities_filter):
- if event.event_type == EVENT_STATE_CHANGED:
- entity_id = event.entity_id
- elif event.event_type in HOMEASSISTANT_EVENTS:
- entity_id = f"{HA_DOMAIN}."
- elif event.event_type == EVENT_CALL_SERVICE:
- return False
+ if event.event_type in HOMEASSISTANT_EVENTS:
+ return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID)
+
+ entity_id = event.data_entity_id
+ if entity_id:
+ return entities_filter is None or entities_filter(entity_id)
+
+ if event.event_type in hass.data[DOMAIN]:
+ # If the entity_id isn't described, use the domain that describes
+ # the event for filtering.
+ domain = hass.data[DOMAIN][event.event_type][0]
else:
- entity_id = event.data_entity_id
- if not entity_id:
- if event.event_type in hass.data[DOMAIN]:
- # If the entity_id isn't described, use the domain that describes
- # the event for filtering.
- domain = hass.data[DOMAIN][event.event_type][0]
- else:
- domain = event.data_domain
- if domain is None:
- return False
- entity_id = f"{domain}."
-
- return entities_filter is None or entities_filter(entity_id)
-
-
-def _entry_message_from_event(entity_id, domain, event, entity_attr_cache):
- """Convert a state to a message for the logbook."""
- # We pass domain in so we don't have to split entity_id again
- state_state = event.state
-
- if domain in ["device_tracker", "person"]:
- if state_state == STATE_NOT_HOME:
- return "is away"
- return f"is at {state_state}"
-
- if domain == "sun":
- if state_state == sun.STATE_ABOVE_HORIZON:
- return "has risen"
- return "has set"
-
- if domain == "binary_sensor":
- device_class = entity_attr_cache.get(entity_id, ATTR_DEVICE_CLASS, event)
- if device_class == "battery":
- if state_state == STATE_ON:
- return "is low"
- if state_state == STATE_OFF:
- return "is normal"
-
- if device_class == "connectivity":
- if state_state == STATE_ON:
- return "is connected"
- if state_state == STATE_OFF:
- return "is disconnected"
-
- if device_class in ["door", "garage_door", "opening", "window"]:
- if state_state == STATE_ON:
- return "is opened"
- if state_state == STATE_OFF:
- return "is closed"
-
- if device_class == "lock":
- if state_state == STATE_ON:
- return "is unlocked"
- if state_state == STATE_OFF:
- return "is locked"
-
- if device_class == "plug":
- if state_state == STATE_ON:
- return "is plugged in"
- if state_state == STATE_OFF:
- return "is unplugged"
-
- if device_class == "presence":
- if state_state == STATE_ON:
- return "is at home"
- if state_state == STATE_OFF:
- return "is away"
-
- if device_class == "safety":
- if state_state == STATE_ON:
- return "is unsafe"
- if state_state == STATE_OFF:
- return "is safe"
-
- if device_class in [
- "cold",
- "gas",
- "heat",
- "light",
- "moisture",
- "motion",
- "occupancy",
- "power",
- "problem",
- "smoke",
- "sound",
- "vibration",
- ]:
- if state_state == STATE_ON:
- return f"detected {device_class}"
- if state_state == STATE_OFF:
- return f"cleared (no {device_class} detected)"
-
- if state_state == STATE_ON:
- # Future: combine groups and its entity entries ?
- return "turned on"
-
- if state_state == STATE_OFF:
- return "turned off"
-
- return f"changed to {state_state}"
+ domain = event.data_domain
+
+ if domain is None:
+ return False
+
+ return entities_filter is None or entities_filter(f"{domain}.")
def _augment_data_with_context(
@@ -697,7 +667,6 @@ class LazyEventPartialState:
__slots__ = [
"_row",
"_event_data",
- "_time_fired",
"_time_fired_isoformat",
"_attributes",
"event_type",
@@ -713,7 +682,6 @@ def __init__(self, row):
"""Init the lazy event."""
self._row = row
self._event_data = None
- self._time_fired = None
self._time_fired_isoformat = None
self._attributes = None
self.event_type = self._row.event_type
@@ -724,6 +692,15 @@ def __init__(self, row):
self.context_user_id = self._row.context_user_id
self.time_fired_minute = self._row.time_fired.minute
+ @property
+ def attributes_icon(self):
+ """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)
+ return result and result.group(1)
+
@property
def data_entity_id(self):
"""Extract the entity id from the decoded data or json."""
@@ -765,25 +742,14 @@ def data(self):
self._event_data = json.loads(self._row.event_data)
return self._event_data
- @property
- def time_fired(self):
- """Time event was fired in utc."""
- if not self._time_fired:
- self._time_fired = (
- process_timestamp(self._row.time_fired) or dt_util.utcnow()
- )
- return self._time_fired
-
@property
def time_fired_isoformat(self):
"""Time event was fired in utc isoformat."""
if not self._time_fired_isoformat:
- if self._time_fired:
- self._time_fired_isoformat = self._time_fired.isoformat()
- else:
- self._time_fired_isoformat = process_timestamp_to_utc_isoformat(
- self._row.time_fired or dt_util.utcnow()
- )
+ self._time_fired_isoformat = process_timestamp_to_utc_isoformat(
+ self._row.time_fired or dt_util.utcnow()
+ )
+
return self._time_fired_isoformat
diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py
index 9d71b3d263a743..d4fb1d5f7bc388 100644
--- a/homeassistant/components/luci/device_tracker.py
+++ b/homeassistant/components/luci/device_tracker.py
@@ -94,6 +94,12 @@ def _update_info(self):
last_results = []
for device in result:
- last_results.append(device)
+ if (
+ not hasattr(self.router.router.owrt_version, "release")
+ or not self.router.router.owrt_version.release
+ or self.router.router.owrt_version.release[0] < 19
+ or device.reachable
+ ):
+ last_results.append(device)
self.last_results = last_results
diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json
index d18cbae51035d6..3b51aab6e4acbb 100644
--- a/homeassistant/components/luci/manifest.json
+++ b/homeassistant/components/luci/manifest.json
@@ -3,5 +3,5 @@
"name": "OpenWRT (luci)",
"documentation": "https://www.home-assistant.io/integrations/luci",
"requirements": ["openwrt-luci-rpc==1.1.6"],
- "codeowners": ["@fbradyirl", "@mzdrale"]
+ "codeowners": ["@mzdrale"]
}
diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py
index 9d18496913911b..91e9c96d42936b 100644
--- a/homeassistant/components/luftdaten/__init__.py
+++ b/homeassistant/components/luftdaten/__init__.py
@@ -13,6 +13,7 @@
CONF_SENSORS,
CONF_SHOW_ON_MAP,
PERCENTAGE,
+ PRESSURE_PA,
TEMP_CELSIUS,
)
from homeassistant.core import callback
@@ -44,8 +45,8 @@
SENSORS = {
SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS],
SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", PERCENTAGE],
- SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"],
- SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", "Pa"],
+ SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", PRESSURE_PA],
+ SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", PRESSURE_PA],
SENSOR_PM10: [
"PM10",
"mdi:thought-bubble",
diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py
index e2e143da4352ee..f77a2b120daced 100644
--- a/homeassistant/components/lutron/binary_sensor.py
+++ b/homeassistant/components/lutron/binary_sensor.py
@@ -51,6 +51,4 @@ def name(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
- attr["lutron_integration_id"] = self._lutron_device.id
- return attr
+ return {"lutron_integration_id": self._lutron_device.id}
diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py
index 438a433fb0fe31..1ec7c07aac04ed 100644
--- a/homeassistant/components/lutron/cover.py
+++ b/homeassistant/components/lutron/cover.py
@@ -66,6 +66,4 @@ def update(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
- attr["Lutron Integration ID"] = self._lutron_device.id
- return attr
+ return {"Lutron Integration ID": self._lutron_device.id}
diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py
index 2b5bff7d848291..411a1d494df470 100644
--- a/homeassistant/components/lutron/light.py
+++ b/homeassistant/components/lutron/light.py
@@ -71,8 +71,7 @@ def turn_off(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {"lutron_integration_id": self._lutron_device.id}
- return attr
+ return {"lutron_integration_id": self._lutron_device.id}
@property
def is_on(self):
diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py
index d03cb4a19530ee..ac00faaa9bcc19 100644
--- a/homeassistant/components/lutron/switch.py
+++ b/homeassistant/components/lutron/switch.py
@@ -48,9 +48,7 @@ def turn_off(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {}
- attr["lutron_integration_id"] = self._lutron_device.id
- return attr
+ return {"lutron_integration_id": self._lutron_device.id}
@property
def is_on(self):
@@ -83,12 +81,11 @@ def turn_off(self, **kwargs):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {
+ return {
"keypad": self._keypad_name,
"scene": self._scene_name,
"led": self._lutron_device.name,
}
- return attr
@property
def is_on(self):
diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py
index 40b65293f1d266..37a9d12af957f5 100644
--- a/homeassistant/components/lutron_caseta/__init__.py
+++ b/homeassistant/components/lutron_caseta/__init__.py
@@ -64,9 +64,9 @@ async def async_setup_entry(hass, config_entry):
"""Set up a bridge from a config entry."""
host = config_entry.data[CONF_HOST]
- keyfile = config_entry.data[CONF_KEYFILE]
- certfile = config_entry.data[CONF_CERTFILE]
- ca_certs = config_entry.data[CONF_CA_CERTS]
+ keyfile = hass.config.path(config_entry.data[CONF_KEYFILE])
+ certfile = hass.config.path(config_entry.data[CONF_CERTFILE])
+ ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS])
bridge = Smartbridge.create_tls(
hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs
diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py
index 3a5a4a151a1450..1290d88b09c590 100644
--- a/homeassistant/components/lutron_caseta/config_flow.py
+++ b/homeassistant/components/lutron_caseta/config_flow.py
@@ -83,9 +83,9 @@ async def async_validate_connectable_bridge_config(self):
try:
bridge = Smartbridge.create_tls(
hostname=self.data[CONF_HOST],
- keyfile=self.data[CONF_KEYFILE],
- certfile=self.data[CONF_CERTFILE],
- ca_certs=self.data[CONF_CA_CERTS],
+ keyfile=self.hass.config.path(self.data[CONF_KEYFILE]),
+ certfile=self.hass.config.path(self.data[CONF_CERTFILE]),
+ ca_certs=self.hass.config.path(self.data[CONF_CA_CERTS]),
)
await bridge.connect()
diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py
index 81a65786900be7..5eabfcd16fe19f 100644
--- a/homeassistant/components/lutron_caseta/cover.py
+++ b/homeassistant/components/lutron_caseta/cover.py
@@ -7,6 +7,7 @@
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
+ SUPPORT_STOP,
CoverEntity,
)
@@ -39,7 +40,7 @@ class LutronCasetaCover(LutronCasetaDevice, CoverEntity):
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
+ return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION
@property
def is_closed(self):
@@ -51,19 +52,27 @@ def current_cover_position(self):
"""Return the current position of cover."""
return self._device["current_state"]
+ async def async_stop_cover(self, **kwargs):
+ """Top the cover."""
+ await self._smartbridge.stop_cover(self.device_id)
+
async def async_close_cover(self, **kwargs):
"""Close the cover."""
- self._smartbridge.set_value(self.device_id, 0)
+ await self._smartbridge.lower_cover(self.device_id)
+ self.async_update()
+ self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
- self._smartbridge.set_value(self.device_id, 100)
+ await self._smartbridge.raise_cover(self.device_id)
+ self.async_update()
+ self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs):
"""Move the shade to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
- self._smartbridge.set_value(self.device_id, position)
+ await self._smartbridge.set_value(self.device_id, position)
async def async_update(self):
"""Call when forcing a refresh of the device."""
diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py
index aa6ab1c714490f..90728c5f2fe55c 100644
--- a/homeassistant/components/lutron_caseta/fan.py
+++ b/homeassistant/components/lutron_caseta/fan.py
@@ -84,7 +84,7 @@ async def async_turn_off(self, **kwargs):
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
- self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed])
+ await self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed])
@property
def is_on(self):
diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py
index 471be51219bb18..c46e8931390b32 100644
--- a/homeassistant/components/lutron_caseta/light.py
+++ b/homeassistant/components/lutron_caseta/light.py
@@ -1,10 +1,13 @@
"""Support for Lutron Caseta lights."""
+from datetime import timedelta
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_TRANSITION,
DOMAIN,
SUPPORT_BRIGHTNESS,
+ SUPPORT_TRANSITION,
LightEntity,
)
@@ -47,21 +50,31 @@ class LutronCasetaLight(LutronCasetaDevice, LightEntity):
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_BRIGHTNESS
+ return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def brightness(self):
"""Return the brightness of the light."""
return to_hass_level(self._device["current_state"])
+ async def _set_brightness(self, brightness, **kwargs):
+ args = {}
+ if ATTR_TRANSITION in kwargs:
+ args["fade_time"] = timedelta(seconds=kwargs[ATTR_TRANSITION])
+
+ await self._smartbridge.set_value(
+ self.device_id, to_lutron_level(brightness), **args
+ )
+
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
- brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
- self._smartbridge.set_value(self.device_id, to_lutron_level(brightness))
+ brightness = kwargs.pop(ATTR_BRIGHTNESS, 255)
+
+ await self._set_brightness(brightness, **kwargs)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
- self._smartbridge.set_value(self.device_id, 0)
+ await self._set_brightness(0, **kwargs)
@property
def is_on(self):
diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json
index 34fc326425cc3c..f80e34a6b5c158 100644
--- a/homeassistant/components/lutron_caseta/manifest.json
+++ b/homeassistant/components/lutron_caseta/manifest.json
@@ -3,9 +3,9 @@
"name": "Lutron Caséta",
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
"requirements": [
- "pylutron-caseta==0.6.1"
+ "pylutron-caseta==0.7.0"
],
"codeowners": [
"@swails"
]
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py
index c74f60bc88ce97..a69f4594d14760 100644
--- a/homeassistant/components/lutron_caseta/scene.py
+++ b/homeassistant/components/lutron_caseta/scene.py
@@ -43,4 +43,4 @@ def name(self):
async def async_activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
- self._bridge.activate_scene(self._scene_id)
+ await self._bridge.activate_scene(self._scene_id)
diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json
index 082497b1bf26a4..a03cdd8c9d66ae 100644
--- a/homeassistant/components/lutron_caseta/strings.json
+++ b/homeassistant/components/lutron_caseta/strings.json
@@ -7,11 +7,11 @@
}
},
"error": {
- "cannot_connect": "Failed to connect to Caséta bridge; check your host and certificate configuration."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "Caséta bridge already configured.",
- "cannot_connect": "Cancelled setup of Caséta bridge due to connection failure."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py
index d7f9246feeb0f0..1cccd485524bad 100644
--- a/homeassistant/components/lutron_caseta/switch.py
+++ b/homeassistant/components/lutron_caseta/switch.py
@@ -32,11 +32,11 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
- self._smartbridge.turn_on(self.device_id)
+ await self._smartbridge.turn_on(self.device_id)
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
- self._smartbridge.turn_off(self.device_id)
+ await self._smartbridge.turn_off(self.device_id)
@property
def is_on(self):
diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json
index 4b6be6e3a6ed96..c3b0e686cc4616 100644
--- a/homeassistant/components/lutron_caseta/translations/ca.json
+++ b/homeassistant/components/lutron_caseta/translations/ca.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "L'enlla\u00e7 de Cas\u00e9ta ja configurat.",
- "cannot_connect": "S'ha cancel\u00b7lat la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta per un error en la connexi\u00f3."
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar a l'enlla\u00e7 de Cas\u00e9ta; comprova la configuraci\u00f3 de l'amfitri\u00f3 i del certificat."
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"import_failed": {
diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json
index 469bcae37c7957..3797d476db3495 100644
--- a/homeassistant/components/lutron_caseta/translations/en.json
+++ b/homeassistant/components/lutron_caseta/translations/en.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Cas\u00e9ta bridge already configured.",
- "cannot_connect": "Cancelled setup of Cas\u00e9ta bridge due to connection failure."
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect"
},
"error": {
- "cannot_connect": "Failed to connect to Cas\u00e9ta bridge; check your host and certificate configuration."
+ "cannot_connect": "Failed to connect"
},
"step": {
"import_failed": {
diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json
new file mode 100644
index 00000000000000..72a54b6b97742a
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Cas\u00e9ta Bridge seadmega \u00fchenduse loomine nurjus. Kontrolli hosti ja serdi s\u00e4tteid."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json
index 02c48b586f206b..0674172e97581f 100644
--- a/homeassistant/components/lutron_caseta/translations/fr.json
+++ b/homeassistant/components/lutron_caseta/translations/fr.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Pont Cas\u00e9ta d\u00e9j\u00e0 configur\u00e9.",
+ "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion."
+ },
"error": {
"cannot_connect": "\u00c9chec de la connexion \u00e0 la passerelle Cas\u00e9ta; v\u00e9rifiez la configuration de votre h\u00f4te et de votre certificat."
},
diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json
index 9e4cb9ec02cce7..5bdcf87607dccc 100644
--- a/homeassistant/components/lutron_caseta/translations/it.json
+++ b/homeassistant/components/lutron_caseta/translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Il bridge Cas\u00e9ta \u00e8 gi\u00e0 configurato.",
- "cannot_connect": "Configurazione annullata del bridge Cas\u00e9ta a causa di un errore di connessione."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi"
},
"error": {
- "cannot_connect": "Impossibile connettersi al bridge Cas\u00e9ta; controllare la configurazione dell'host e del certificato."
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"import_failed": {
diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json
index b63e8fb4111d26..b4cdadd2d2fef4 100644
--- a/homeassistant/components/lutron_caseta/translations/no.json
+++ b/homeassistant/components/lutron_caseta/translations/no.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Cas\u00e9ta bridge allerede konfigurert.",
- "cannot_connect": "Avbrutt oppsett av Cas\u00e9ta bridge p\u00e5 grunn av tilkoblingssvikt."
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes."
},
"error": {
- "cannot_connect": "Kunne ikke koble til Cas\u00e9ta bridge; sjekk verts- og sertifikatkonfigurasjonen."
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"import_failed": {
diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json
index f7f566b1f543b2..05bd4f51c70bf1 100644
--- a/homeassistant/components/lutron_caseta/translations/ru.json
+++ b/homeassistant/components/lutron_caseta/translations/ru.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "cannot_connect": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u0437-\u0437\u0430 \u0441\u0431\u043e\u044f \u0432 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0438."
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"import_failed": {
diff --git a/homeassistant/components/mailgun/strings.json b/homeassistant/components/mailgun/strings.json
index 29ea3c0b952a6f..a948c6165e7977 100644
--- a/homeassistant/components/mailgun/strings.json
+++ b/homeassistant/components/mailgun/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages."
},
"create_entry": {
diff --git a/homeassistant/components/mailgun/translations/ca.json b/homeassistant/components/mailgun/translations/ca.json
index 629205b276d0c2..496894090a7ab2 100644
--- a/homeassistant/components/mailgun/translations/ca.json
+++ b/homeassistant/components/mailgun/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/json\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
diff --git a/homeassistant/components/mailgun/translations/el.json b/homeassistant/components/mailgun/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/en.json b/homeassistant/components/mailgun/translations/en.json
index 85044505c84f09..bcb3556087dd7d 100644
--- a/homeassistant/components/mailgun/translations/en.json
+++ b/homeassistant/components/mailgun/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json
index f2e5638dcc338e..4b9762c8b92514 100644
--- a/homeassistant/components/mailgun/translations/es.json
+++ b/homeassistant/components/mailgun/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Mailgun.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/json \n\n Consulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes."
diff --git a/homeassistant/components/mailgun/translations/et.json b/homeassistant/components/mailgun/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json
index 266c48f91a5e57..edb9f01be3deb3 100644
--- a/homeassistant/components/mailgun/translations/fr.json
+++ b/homeassistant/components/mailgun/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages Mailgun.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun]({mailgun_url}). \n\n Remplissez les informations suivantes: \n\n - URL: `{webhook_url}` \n - M\u00e9thode: POST \n - Type de contenu: application/json \n\n Voir [la documentation]({docs_url}) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes."
diff --git a/homeassistant/components/mailgun/translations/it.json b/homeassistant/components/mailgun/translations/it.json
index 624301a14d19a3..0373d686744c72 100644
--- a/homeassistant/components/mailgun/translations/it.json
+++ b/homeassistant/components/mailgun/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Mailgun.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo."
diff --git a/homeassistant/components/mailgun/translations/lb.json b/homeassistant/components/mailgun/translations/lb.json
index 10504b88214ced..42e3c98d8378d3 100644
--- a/homeassistant/components/mailgun/translations/lb.json
+++ b/homeassistant/components/mailgun/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
diff --git a/homeassistant/components/mailgun/translations/no.json b/homeassistant/components/mailgun/translations/no.json
index 9f26892e703ba0..a649df5a968bc8 100644
--- a/homeassistant/components/mailgun/translations/no.json
+++ b/homeassistant/components/mailgun/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Mailgun-meldinger.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data."
diff --git a/homeassistant/components/mailgun/translations/ru.json b/homeassistant/components/mailgun/translations/ru.json
index 1d469c4692defa..f8a7c4518191c7 100644
--- a/homeassistant/components/mailgun/translations/ru.json
+++ b/homeassistant/components/mailgun/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Mailgun.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445."
diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json
index cd39d7a4d83fd5..51b859cb73fead 100644
--- a/homeassistant/components/mailgun/translations/zh-Hant.json
+++ b/homeassistant/components/mailgun/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002"
diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py
index 4e361b5086c7ba..168d5b637e920e 100644
--- a/homeassistant/components/manual/alarm_control_panel.py
+++ b/homeassistant/components/manual/alarm_control_panel.py
@@ -394,13 +394,12 @@ def _validate_code(self, code, state):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- state_attr = {}
-
if self.state == STATE_ALARM_PENDING or self.state == STATE_ALARM_ARMING:
- state_attr[ATTR_PREVIOUS_STATE] = self._previous_state
- state_attr[ATTR_NEXT_STATE] = self._state
-
- return state_attr
+ return {
+ ATTR_PREVIOUS_STATE: self._previous_state,
+ ATTR_NEXT_STATE: self._state,
+ }
+ return {}
@callback
def async_scheduled_update(self, now):
diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py
index 4c760d3dab029b..10c2a473a29562 100644
--- a/homeassistant/components/manual_mqtt/alarm_control_panel.py
+++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py
@@ -415,13 +415,12 @@ def _validate_code(self, code, state):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- state_attr = {}
-
- if self.state == STATE_ALARM_PENDING:
- state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
- state_attr[ATTR_POST_PENDING_STATE] = self._state
-
- return state_attr
+ if self.state != STATE_ALARM_PENDING:
+ return {}
+ return {
+ ATTR_PRE_PENDING_STATE: self._previous_state,
+ ATTR_POST_PENDING_STATE: self._state,
+ }
async def async_added_to_hass(self):
"""Subscribe to MQTT events."""
diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py
index b42c96f99c2fec..312f55ce0d8aff 100644
--- a/homeassistant/components/maxcube/binary_sensor.py
+++ b/homeassistant/components/maxcube/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for MAX! binary sensors via MAX! Cube."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_WINDOW,
+ BinarySensorEntity,
+)
from . import DATA_KEY
@@ -30,16 +33,11 @@ class MaxCubeShutter(BinarySensorEntity):
def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube BinarySensorEntity."""
self._name = name
- self._sensor_type = "window"
+ self._sensor_type = DEVICE_CLASS_WINDOW
self._rf_address = rf_address
self._cubehandle = handler
self._state = None
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def name(self):
"""Return the name of the BinarySensorEntity."""
diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py
index 69d9177da5dcfd..e222784ca57151 100644
--- a/homeassistant/components/maxcube/climate.py
+++ b/homeassistant/components/maxcube/climate.py
@@ -40,6 +40,11 @@
# On (valve fully open)
ON_TEMPERATURE = 30.5
+# Lowest Value without turning off
+MIN_TEMPERATURE = 5.0
+# Largest Value without fully opening
+MAX_TEMPERATURE = 30.0
+
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
HASS_PRESET_TO_MAX_MODE = {
@@ -100,13 +105,17 @@ def name(self):
def min_temp(self):
"""Return the minimum temperature."""
device = self._cubehandle.cube.device_by_rf(self._rf_address)
- return self.map_temperature_max_hass(device.min_temperature)
+ if device.min_temperature is None:
+ return MIN_TEMPERATURE
+ return device.min_temperature
@property
def max_temp(self):
"""Return the maximum temperature."""
device = self._cubehandle.cube.device_by_rf(self._rf_address)
- return self.map_temperature_max_hass(device.max_temperature)
+ if device.max_temperature is None:
+ return MAX_TEMPERATURE
+ return device.max_temperature
@property
def temperature_unit(self):
@@ -117,9 +126,7 @@ def temperature_unit(self):
def current_temperature(self):
"""Return the current temperature."""
device = self._cubehandle.cube.device_by_rf(self._rf_address)
-
- # Map and return current temperature
- return self.map_temperature_max_hass(device.actual_temperature)
+ return device.actual_temperature
@property
def hvac_mode(self):
@@ -195,7 +202,13 @@ def hvac_action(self):
def target_temperature(self):
"""Return the temperature we try to reach."""
device = self._cubehandle.cube.device_by_rf(self._rf_address)
- return self.map_temperature_max_hass(device.target_temperature)
+ if (
+ device.target_temperature is None
+ or device.target_temperature < self.min_temp
+ or device.target_temperature > self.max_temp
+ ):
+ return None
+ return device.target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
@@ -273,21 +286,11 @@ def device_state_attributes(self):
"""Return the optional state attributes."""
cube = self._cubehandle.cube
device = cube.device_by_rf(self._rf_address)
- attributes = {}
- if cube.is_thermostat(device):
- attributes[ATTR_VALVE_POSITION] = device.valve_position
-
- return attributes
+ if not cube.is_thermostat(device):
+ return {}
+ return {ATTR_VALVE_POSITION: device.valve_position}
def update(self):
"""Get latest data from MAX! Cube."""
self._cubehandle.update()
-
- @staticmethod
- def map_temperature_max_hass(temperature):
- """Map Temperature from MAX! to Home Assistant."""
- if temperature is None:
- return 0.0
-
- return temperature
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 62d53b17c47980..1dbb642aee9d2b 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -2,7 +2,7 @@
"domain": "media_extractor",
"name": "Media Extractor",
"documentation": "https://www.home-assistant.io/integrations/media_extractor",
- "requirements": ["youtube_dl==2020.07.28"],
+ "requirements": ["youtube_dl==2020.09.20"],
"dependencies": ["media_player"],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 348bc521a5a8c7..1bf0e213a25246 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -27,6 +27,7 @@
HTTP_INTERNAL_SERVER_ERROR,
HTTP_NOT_FOUND,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
@@ -71,6 +72,7 @@
ATTR_MEDIA_DURATION,
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EPISODE,
+ ATTR_MEDIA_EXTRA,
ATTR_MEDIA_PLAYLIST,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
@@ -139,6 +141,7 @@
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
+ vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
}
ATTR_TO_PROPERTY = [
@@ -880,7 +883,7 @@ async def get(self, request: web.Request, entity_id: str) -> web.Response:
"""Start a get request."""
player = self.component.get_entity(entity_id)
if player is None:
- status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED] else 401
+ status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED] else HTTP_UNAUTHORIZED
return web.Response(status=status)
authenticated = (
@@ -889,7 +892,7 @@ async def get(self, request: web.Request, entity_id: str) -> web.Response:
)
if not authenticated:
- return web.Response(status=401)
+ return web.Response(status=HTTP_UNAUTHORIZED)
data, content_type = await player.async_get_media_image()
diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py
index 0035fc9f4d27ac..3db3100634133b 100644
--- a/homeassistant/components/media_player/const.py
+++ b/homeassistant/components/media_player/const.py
@@ -12,6 +12,7 @@
ATTR_MEDIA_CONTENT_TYPE = "media_content_type"
ATTR_MEDIA_DURATION = "media_duration"
ATTR_MEDIA_ENQUEUE = "enqueue"
+ATTR_MEDIA_EXTRA = "extra"
ATTR_MEDIA_EPISODE = "media_episode"
ATTR_MEDIA_PLAYLIST = "media_playlist"
ATTR_MEDIA_POSITION = "media_position"
diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py
new file mode 100644
index 00000000000000..b612165fa19d88
--- /dev/null
+++ b/homeassistant/components/media_player/group.py
@@ -0,0 +1,17 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import STATE_IDLE, STATE_PLAYING
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_PLAYING, STATE_IDLE}, STATE_OFF)
diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py
index a90e4fffdc1fb0..64955d1913ba30 100644
--- a/homeassistant/components/media_player/reproduce_state.py
+++ b/homeassistant/components/media_player/reproduce_state.py
@@ -5,7 +5,6 @@
from homeassistant.const import (
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
- SERVICE_MEDIA_SEEK,
SERVICE_MEDIA_STOP,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -25,7 +24,6 @@
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_ENQUEUE,
- ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_SOUND_MODE,
@@ -58,16 +56,18 @@ async def call_service(service: str, keys: Iterable) -> None:
DOMAIN, service, data, blocking=True, context=context
)
- if state.state == STATE_ON:
- await call_service(SERVICE_TURN_ON, [])
- elif state.state == STATE_OFF:
+ if state.state == STATE_OFF:
await call_service(SERVICE_TURN_OFF, [])
- elif state.state == STATE_PLAYING:
- await call_service(SERVICE_MEDIA_PLAY, [])
- elif state.state == STATE_IDLE:
- await call_service(SERVICE_MEDIA_STOP, [])
- elif state.state == STATE_PAUSED:
- await call_service(SERVICE_MEDIA_PAUSE, [])
+ # entities that are off have no other attributes to restore
+ return
+
+ if state.state in [
+ STATE_ON,
+ STATE_PLAYING,
+ STATE_IDLE,
+ STATE_PAUSED,
+ ]:
+ await call_service(SERVICE_TURN_ON, [])
if ATTR_MEDIA_VOLUME_LEVEL in state.attributes:
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
@@ -75,15 +75,14 @@ async def call_service(service: str, keys: Iterable) -> None:
if ATTR_MEDIA_VOLUME_MUTED in state.attributes:
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
- if ATTR_MEDIA_SEEK_POSITION in state.attributes:
- await call_service(SERVICE_MEDIA_SEEK, [ATTR_MEDIA_SEEK_POSITION])
-
if ATTR_INPUT_SOURCE in state.attributes:
await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE])
if ATTR_SOUND_MODE in state.attributes:
await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE])
+ already_playing = False
+
if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and (
ATTR_MEDIA_CONTENT_ID in state.attributes
):
@@ -91,6 +90,14 @@ async def call_service(service: str, keys: Iterable) -> None:
SERVICE_PLAY_MEDIA,
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
)
+ already_playing = True
+
+ if state.state == STATE_PLAYING and not already_playing:
+ await call_service(SERVICE_MEDIA_PLAY, [])
+ elif state.state == STATE_IDLE:
+ await call_service(SERVICE_MEDIA_STOP, [])
+ elif state.state == STATE_PAUSED:
+ await call_service(SERVICE_MEDIA_PAUSE, [])
async def async_reproduce_states(
diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json
index 2800870e9ccf38..4d71a30a8ac2f8 100644
--- a/homeassistant/components/media_player/translations/et.json
+++ b/homeassistant/components/media_player/translations/et.json
@@ -1,4 +1,13 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} on j\u00f5udeolekus",
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud",
+ "is_paused": "{entity_name} on peatatud",
+ "is_playing": "{entity_name} m\u00e4ngib"
+ }
+ },
"state": {
"_": {
"idle": "Ootel",
diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py
index 68a8244c3cedd1..739af47e653bbf 100644
--- a/homeassistant/components/media_source/const.py
+++ b/homeassistant/components/media_source/const.py
@@ -15,4 +15,6 @@
"image": MEDIA_CLASS_IMAGE,
}
URI_SCHEME = "media-source://"
-URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?")
+URI_SCHEME_REGEX = re.compile(
+ r"^media-source:\/\/(?:(?P(?!.+__)(?!_)[\da-z_]+(?(?!\/).+))?)?$"
+)
diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py
index a558de775f8aed..6c60da562e08de 100644
--- a/homeassistant/components/media_source/local_source.py
+++ b/homeassistant/components/media_source/local_source.py
@@ -21,26 +21,7 @@ def async_setup(hass: HomeAssistant):
"""Set up local media source."""
source = LocalSource(hass)
hass.data[DOMAIN][DOMAIN] = source
- hass.http.register_view(LocalMediaView(hass))
-
-
-@callback
-def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]:
- """Parse identifier."""
- if not item.identifier:
- source_dir_id = "media"
- location = ""
-
- else:
- source_dir_id, location = item.identifier.lstrip("/").split("/", 1)
-
- if source_dir_id != "media":
- raise Unresolvable("Unknown source directory.")
-
- if location != sanitize_path(location):
- raise Unresolvable("Invalid path.")
-
- return source_dir_id, location
+ hass.http.register_view(LocalMediaView(hass, source))
class LocalSource(MediaSource):
@@ -56,22 +37,41 @@ def __init__(self, hass: HomeAssistant):
@callback
def async_full_path(self, source_dir_id, location) -> Path:
"""Return full path."""
- return self.hass.config.path("media", location)
+ return Path(self.hass.config.media_dirs[source_dir_id], location)
+
+ @callback
+ def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]:
+ """Parse identifier."""
+ if not item.identifier:
+ # Empty source_dir_id and location
+ return "", ""
+
+ source_dir_id, location = item.identifier.split("/", 1)
+ if source_dir_id not in self.hass.config.media_dirs:
+ raise Unresolvable("Unknown source directory.")
+
+ if location != sanitize_path(location):
+ raise Unresolvable("Invalid path.")
+
+ return source_dir_id, location
async def async_resolve_media(self, item: MediaSourceItem) -> str:
"""Resolve media to a url."""
- source_dir_id, location = async_parse_identifier(item)
+ source_dir_id, location = self.async_parse_identifier(item)
+ if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs:
+ raise Unresolvable("Unknown source directory.")
+
mime_type, _ = mimetypes.guess_type(
- self.async_full_path(source_dir_id, location)
+ str(self.async_full_path(source_dir_id, location))
)
- return PlayMedia(item.identifier, mime_type)
+ return PlayMedia(f"/media/{item.identifier}", mime_type)
async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Return media."""
try:
- source_dir_id, location = async_parse_identifier(item)
+ source_dir_id, location = self.async_parse_identifier(item)
except Unresolvable as err:
raise BrowseError(str(err)) from err
@@ -79,9 +79,37 @@ async def async_browse_media(
self._browse_media, source_dir_id, location
)
- def _browse_media(self, source_dir_id, location):
+ def _browse_media(self, source_dir_id: str, location: Path):
"""Browse media."""
- full_path = Path(self.hass.config.path("media", location))
+
+ # If only one media dir is configured, use that as the local media root
+ if source_dir_id == "" and len(self.hass.config.media_dirs) == 1:
+ source_dir_id = list(self.hass.config.media_dirs)[0]
+
+ # Multiple folder, root is requested
+ if source_dir_id == "":
+ if location:
+ raise BrowseError("Folder not found.")
+
+ base = BrowseMediaSource(
+ domain=DOMAIN,
+ identifier="",
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_type=None,
+ title=self.name,
+ can_play=False,
+ can_expand=True,
+ children_media_class=MEDIA_CLASS_DIRECTORY,
+ )
+
+ base.children = [
+ self._browse_media(source_dir_id, "")
+ for source_dir_id in self.hass.config.media_dirs
+ ]
+
+ return base
+
+ full_path = Path(self.hass.config.media_dirs[source_dir_id], location)
if not full_path.exists():
if location == "":
@@ -118,7 +146,7 @@ def _build_item_response(self, source_dir_id: str, path: Path, is_child=False):
media = BrowseMediaSource(
domain=DOMAIN,
- identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
+ identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.media_dirs[source_dir_id])}",
media_class=media_class,
media_content_type=mime_type or "",
title=title,
@@ -149,19 +177,25 @@ class LocalMediaView(HomeAssistantView):
Returns media files in config/media.
"""
- url = "/media/{location:.*}"
+ url = "/media/{source_dir_id}/{location:.*}"
name = "media"
- def __init__(self, hass: HomeAssistant):
+ def __init__(self, hass: HomeAssistant, source: LocalSource):
"""Initialize the media view."""
self.hass = hass
+ self.source = source
- async def get(self, request: web.Request, location: str) -> web.FileResponse:
+ async def get(
+ self, request: web.Request, source_dir_id: str, location: str
+ ) -> web.FileResponse:
"""Start a GET request."""
if location != sanitize_path(location):
- return web.HTTPNotFound()
+ raise web.HTTPNotFound()
+
+ if source_dir_id not in self.hass.config.media_dirs:
+ raise web.HTTPNotFound()
- media_path = Path(self.hass.config.path("media", location))
+ media_path = self.source.async_full_path(source_dir_id, location)
# Check that the file exists
if not media_path.is_file():
diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py
index ed6fc31c41482d..8813883c151462 100644
--- a/homeassistant/components/melcloud/config_flow.py
+++ b/homeassistant/components/melcloud/config_flow.py
@@ -9,7 +9,13 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, HTTP_FORBIDDEN
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_TOKEN,
+ CONF_USERNAME,
+ HTTP_FORBIDDEN,
+ HTTP_UNAUTHORIZED,
+)
from .const import DOMAIN # pylint: disable=unused-import
@@ -57,7 +63,7 @@ async def _create_client(
self.hass.helpers.aiohttp_client.async_get_clientsession(),
)
except ClientResponseError as err:
- if err.status == 401 or err.status == HTTP_FORBIDDEN:
+ if err.status == HTTP_UNAUTHORIZED or err.status == HTTP_FORBIDDEN:
return self.async_abort(reason="invalid_auth")
return self.async_abort(reason="cannot_connect")
except (asyncio.TimeoutError, ClientError):
diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json
index c4161a87ff80f5..a1bce80d7ad425 100644
--- a/homeassistant/components/melcloud/strings.json
+++ b/homeassistant/components/melcloud/strings.json
@@ -11,12 +11,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "MELCloud integration already configured for this email. Access token has been refreshed."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/melcloud/translations/ca.json b/homeassistant/components/melcloud/translations/ca.json
index 2da4f4b57f34c9..b31698d28ce9df 100644
--- a/homeassistant/components/melcloud/translations/ca.json
+++ b/homeassistant/components/melcloud/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El token d'acc\u00e9s s'ha actualitzat."
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/melcloud/translations/en.json b/homeassistant/components/melcloud/translations/en.json
index 6e701fecf5527e..e21fe15f304421 100644
--- a/homeassistant/components/melcloud/translations/en.json
+++ b/homeassistant/components/melcloud/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "MELCloud integration already configured for this email. Access token has been refreshed."
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/melcloud/translations/et.json b/homeassistant/components/melcloud/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/it.json b/homeassistant/components/melcloud/translations/it.json
index 2a3125619324d0..15d9f0d06fc26b 100644
--- a/homeassistant/components/melcloud/translations/it.json
+++ b/homeassistant/components/melcloud/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Integrazione MELCloud gi\u00e0 configurata per questa e-mail. Il token di accesso \u00e8 stato aggiornato."
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/melcloud/translations/no.json b/homeassistant/components/melcloud/translations/no.json
index e96fdb171e7d25..6608b8c5c712c9 100644
--- a/homeassistant/components/melcloud/translations/no.json
+++ b/homeassistant/components/melcloud/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "MELCloud integrasjon er allerede konfigurert p\u00e5 denne e-posten. Access token har blitt oppdatert."
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/melcloud/translations/pl.json b/homeassistant/components/melcloud/translations/pl.json
index 44467601826546..cd0c961089e9e9 100644
--- a/homeassistant/components/melcloud/translations/pl.json
+++ b/homeassistant/components/melcloud/translations/pl.json
@@ -5,8 +5,8 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/melcloud/translations/ru.json b/homeassistant/components/melcloud/translations/ru.json
index 7fd6d31a7539dc..e904ea4e8b7206 100644
--- a/homeassistant/components/melcloud/translations/ru.json
+++ b/homeassistant/components/melcloud/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MELCloud \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/melcloud/translations/zh-Hant.json b/homeassistant/components/melcloud/translations/zh-Hant.json
index 1e6e1d880c72f9..9947b5ac990869 100644
--- a/homeassistant/components/melcloud/translations/zh-Hant.json
+++ b/homeassistant/components/melcloud/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py
index b9a992bb823e6f..6b3d6735f46604 100644
--- a/homeassistant/components/met/config_flow.py
+++ b/homeassistant/components/met/config_flow.py
@@ -47,7 +47,7 @@ async def async_step_user(self, user_input=None):
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
- self._errors[CONF_NAME] = "name_exists"
+ self._errors[CONF_NAME] = "already_configured"
return await self._show_config_form(
name=HOME_LOCATION_NAME,
diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json
index 814df01b49ec00..c3e28d98efffab 100644
--- a/homeassistant/components/met/strings.json
+++ b/homeassistant/components/met/strings.json
@@ -5,13 +5,13 @@
"title": "Location",
"description": "Meteorologisk institutt",
"data": {
- "name": "Name",
- "latitude": "Latitude",
- "longitude": "Longitude",
+ "name": "[%key:common::config_flow::data::name%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
"elevation": "Elevation"
}
}
},
- "error": { "name_exists": "Location already exists" }
+ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
}
}
diff --git a/homeassistant/components/met/translations/ca.json b/homeassistant/components/met/translations/ca.json
index 242232663f0f04..d33e905e721dfa 100644
--- a/homeassistant/components/met/translations/ca.json
+++ b/homeassistant/components/met/translations/ca.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "El servei ja est\u00e0 configurat",
"name_exists": "El nom ja existeix"
},
"step": {
diff --git a/homeassistant/components/met/translations/en.json b/homeassistant/components/met/translations/en.json
index 8c57e4226a054a..012557d917a978 100644
--- a/homeassistant/components/met/translations/en.json
+++ b/homeassistant/components/met/translations/en.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "Service is already configured",
"name_exists": "Location already exists"
},
"step": {
diff --git a/homeassistant/components/met/translations/es.json b/homeassistant/components/met/translations/es.json
index e9cfbce5291832..9306bd03022910 100644
--- a/homeassistant/components/met/translations/es.json
+++ b/homeassistant/components/met/translations/es.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "El servicio ya est\u00e1 configurado",
"name_exists": "La ubicaci\u00f3n ya existe"
},
"step": {
diff --git a/homeassistant/components/met/translations/et.json b/homeassistant/components/met/translations/et.json
new file mode 100644
index 00000000000000..df6a380a5e76fb
--- /dev/null
+++ b/homeassistant/components/met/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Teenus on juba h\u00e4\u00e4lestatud",
+ "name_exists": "See asukoht on juba m\u00e4\u00e4ratud"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "K\u00f5rgus merepinnast",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Nimi"
+ },
+ "description": "Norra ilmateenistus",
+ "title": "Asukoht"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/translations/fr.json b/homeassistant/components/met/translations/fr.json
index 9f43be5d93b0b6..fc712cb69128c2 100644
--- a/homeassistant/components/met/translations/fr.json
+++ b/homeassistant/components/met/translations/fr.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9",
"name_exists": "Emplacement d\u00e9j\u00e0 existant"
},
"step": {
diff --git a/homeassistant/components/met/translations/it.json b/homeassistant/components/met/translations/it.json
index 18433ab1697043..16d68553ab4830 100644
--- a/homeassistant/components/met/translations/it.json
+++ b/homeassistant/components/met/translations/it.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
"name_exists": "La posizione esiste gi\u00e0"
},
"step": {
@@ -8,7 +9,7 @@
"data": {
"elevation": "Altitudine",
"latitude": "Latitudine",
- "longitude": "Longitudine",
+ "longitude": "Logitudine",
"name": "Nome"
},
"description": "Meteorologisk institutt",
diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json
index 90489288b62fe3..937f626e6c2ebf 100644
--- a/homeassistant/components/met/translations/no.json
+++ b/homeassistant/components/met/translations/no.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "Tjenesten er allerede konfigurert",
"name_exists": "Lokasjonen finnes allerede"
},
"step": {
diff --git a/homeassistant/components/met/translations/ru.json b/homeassistant/components/met/translations/ru.json
index 85333490f3ccda..149e085057b8bb 100644
--- a/homeassistant/components/met/translations/ru.json
+++ b/homeassistant/components/met/translations/ru.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "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.",
"name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e."
},
"step": {
diff --git a/homeassistant/components/met/translations/zh-Hant.json b/homeassistant/components/met/translations/zh-Hant.json
index 0128f0ad448931..bb5d2df412a9b5 100644
--- a/homeassistant/components/met/translations/zh-Hant.json
+++ b/homeassistant/components/met/translations/zh-Hant.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"name_exists": "\u8a72\u5ea7\u6a19\u5df2\u5b58\u5728"
},
"step": {
diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py
index 3355c497aab4af..e4c64f9aedab21 100644
--- a/homeassistant/components/met/weather.py
+++ b/homeassistant/components/met/weather.py
@@ -5,6 +5,8 @@
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TIME,
ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_TEMPERATURE,
@@ -209,13 +211,17 @@ def forecast(self):
met_forecast = self.coordinator.data.hourly_forecast
else:
met_forecast = self.coordinator.data.daily_forecast
+ required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME}
ha_forecast = []
for met_item in met_forecast:
+ if not set(met_item).issuperset(required_keys):
+ continue
ha_item = {
k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v)
}
- ha_item[ATTR_FORECAST_CONDITION] = format_condition(
- ha_item[ATTR_FORECAST_CONDITION]
- )
+ if ha_item.get(ATTR_FORECAST_CONDITION):
+ ha_item[ATTR_FORECAST_CONDITION] = format_condition(
+ ha_item[ATTR_FORECAST_CONDITION]
+ )
ha_forecast.append(ha_item)
return ha_forecast
diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
index 8b4d3a33501085..59524ed1a80b54 100644
--- a/homeassistant/components/meteo_france/const.py
+++ b/homeassistant/components/meteo_france/const.py
@@ -4,6 +4,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
@@ -108,7 +109,7 @@
},
"precipitation": {
ENTITY_NAME: "Daily precipitation",
- ENTITY_UNIT: "mm",
+ ENTITY_UNIT: LENGTH_MILLIMETERS,
ENTITY_ICON: "mdi:cup-water",
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json
index 611d1ca054cb93..4deb17d01e686b 100644
--- a/homeassistant/components/meteo_france/strings.json
+++ b/homeassistant/components/meteo_france/strings.json
@@ -21,7 +21,7 @@
},
"abort": {
"already_configured": "City already configured",
- "unknown": "Unknown error: please retry later"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/meteo_france/translations/ca.json b/homeassistant/components/meteo_france/translations/ca.json
index 81f2e6f2d204ac..f38111707407c0 100644
--- a/homeassistant/components/meteo_france/translations/ca.json
+++ b/homeassistant/components/meteo_france/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Ciutat ja configurada",
- "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard"
+ "unknown": "Error inesperat"
},
"error": {
"empty": "No s'ha trobat cap resultat en la cerca de la ciutat: comprova el camp ciutat"
diff --git a/homeassistant/components/meteo_france/translations/en.json b/homeassistant/components/meteo_france/translations/en.json
index 979f705cc5bb8e..02a08504ec4f48 100644
--- a/homeassistant/components/meteo_france/translations/en.json
+++ b/homeassistant/components/meteo_france/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "City already configured",
- "unknown": "Unknown error: please retry later"
+ "unknown": "Unexpected error"
},
"error": {
"empty": "No result in city search: please check the city field"
diff --git a/homeassistant/components/meteo_france/translations/it.json b/homeassistant/components/meteo_france/translations/it.json
index df5cf4a637524f..d9ffd866b1cc9e 100644
--- a/homeassistant/components/meteo_france/translations/it.json
+++ b/homeassistant/components/meteo_france/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Citt\u00e0 gi\u00e0 configurata",
- "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi"
+ "unknown": "Errore imprevisto"
},
"error": {
"empty": "Nessun risultato nella ricerca della citt\u00e0: si prega di controllare il campo citt\u00e0"
diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json
index 166ddaa68ab992..4b8dc3204dd57a 100644
--- a/homeassistant/components/meteo_france/translations/ko.json
+++ b/homeassistant/components/meteo_france/translations/ko.json
@@ -4,7 +4,13 @@
"already_configured": "\ub3c4\uc2dc\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
},
+ "error": {
+ "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c: \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624."
+ },
"step": {
+ "cities": {
+ "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)"
+ },
"user": {
"data": {
"city": "\ub3c4\uc2dc"
@@ -13,5 +19,14 @@
"title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "\uc608\uce21 \ubaa8\ub4dc"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/no.json b/homeassistant/components/meteo_france/translations/no.json
index 91eea1fcec7a32..f0aadd799d69d8 100644
--- a/homeassistant/components/meteo_france/translations/no.json
+++ b/homeassistant/components/meteo_france/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Byen er allerede konfigurert",
- "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere"
+ "unknown": "Uventet feil"
},
"error": {
"empty": "Ingen resultater i bys\u00f8k: vennligst sjekk byfeltet"
diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json
index 46f3c1fdc27558..ea9e0b1312e8c4 100644
--- a/homeassistant/components/meteo_france/translations/pl.json
+++ b/homeassistant/components/meteo_france/translations/pl.json
@@ -4,7 +4,17 @@
"already_configured": "Miasto jest ju\u017c skonfigurowane.",
"unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej."
},
+ "error": {
+ "empty": "Brak wynik\u00f3w: sprawd\u017a nazw\u0119 w polu miasta"
+ },
"step": {
+ "cities": {
+ "data": {
+ "city": "Miasto"
+ },
+ "description": "Wybierz swoje miasto z listy",
+ "title": "M\u00e9t\u00e9o-France"
+ },
"user": {
"data": {
"city": "Miasto"
@@ -13,5 +23,14 @@
"title": "M\u00e9t\u00e9o-France"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Tryb prognozy"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/ru.json b/homeassistant/components/meteo_france/translations/ru.json
index ba0bf1df3c2534..7b0bb4e88a2c6b 100644
--- a/homeassistant/components/meteo_france/translations/ru.json
+++ b/homeassistant/components/meteo_france/translations/ru.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c\u0438 \u0436\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435."
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"empty": "\u041d\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043f\u043e\u0438\u0441\u043a\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u0413\u043e\u0440\u043e\u0434\"."
diff --git a/homeassistant/components/meteo_france/translations/zh-Hant.json b/homeassistant/components/meteo_france/translations/zh-Hant.json
index 0179f5ad7d1856..ac2b11dbab1358 100644
--- a/homeassistant/components/meteo_france/translations/zh-Hant.json
+++ b/homeassistant/components/meteo_france/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u57ce\u5e02\u5df2\u8a2d\u5b9a\u5b8c\u6210",
- "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66"
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
"empty": "\u627e\u4e0d\u5230\u76f8\u7b26\u7684\u57ce\u5e02\uff1a\u8acb\u78ba\u8a8d\u57ce\u5e02\u6b04\u4f4d"
diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py
index b481b417b9ea50..6b13d03ebbad44 100644
--- a/homeassistant/components/meteoalarm/binary_sensor.py
+++ b/homeassistant/components/meteoalarm/binary_sensor.py
@@ -5,7 +5,11 @@
from meteoalertapi import Meteoalert
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_SAFETY,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
import homeassistant.helpers.config_validation as cv
@@ -17,7 +21,6 @@
CONF_LANGUAGE = "language"
CONF_PROVINCE = "province"
-DEFAULT_DEVICE_CLASS = "safety"
DEFAULT_NAME = "meteoalarm"
SCAN_INTERVAL = timedelta(minutes=30)
@@ -78,7 +81,7 @@ def device_state_attributes(self):
@property
def device_class(self):
"""Return the device class of this binary sensor."""
- return DEFAULT_DEVICE_CLASS
+ return DEVICE_CLASS_SAFETY
def update(self):
"""Update device state."""
diff --git a/homeassistant/components/metoffice/strings.json b/homeassistant/components/metoffice/strings.json
index 74d8b16542a048..5a1c59bcfb72fb 100644
--- a/homeassistant/components/metoffice/strings.json
+++ b/homeassistant/components/metoffice/strings.json
@@ -5,9 +5,9 @@
"description": "The latitude and longitude will be used to find the closest weather station.",
"title": "Connect to the UK Met Office",
"data": {
- "api_key": "Met Office DataPoint API key",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
@@ -16,7 +16,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json
new file mode 100644
index 00000000000000..55896dc4901fce
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/et.json b/homeassistant/components/metoffice/translations/et.json
new file mode 100644
index 00000000000000..c6ad082c40e2df
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ },
+ "description": "Laius- ja pikkuskraadi kasutatakse l\u00e4hima ilmajaama leidmiseks."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/pl.json b/homeassistant/components/metoffice/translations/pl.json
index 7167faf5494f34..6b129f2965c779 100644
--- a/homeassistant/components/metoffice/translations/pl.json
+++ b/homeassistant/components/metoffice/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py
index d2ba2371303bba..21963140547f14 100644
--- a/homeassistant/components/mfi/switch.py
+++ b/homeassistant/components/mfi/switch.py
@@ -69,11 +69,6 @@ def __init__(self, port):
self._port = port
self._target_state = None
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def unique_id(self):
"""Return the unique ID of the device."""
@@ -114,7 +109,7 @@ def current_power_w(self):
@property
def device_state_attributes(self):
"""Return the state attributes for the device."""
- attr = {}
- attr["volts"] = round(self._port.data.get("v_rms", 0), 1)
- attr["amps"] = round(self._port.data.get("i_rms", 0), 1)
- return attr
+ 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/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py
index 208cecf6d3b945..69a738724c3225 100644
--- a/homeassistant/components/microsoft_face/__init__.py
+++ b/homeassistant/components/microsoft_face/__init__.py
@@ -8,7 +8,7 @@
import async_timeout
import voluptuous as vol
-from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT
+from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -290,7 +290,7 @@ async def call_api(self, method, function, data=None, binary=False, params=None)
headers[CONTENT_TYPE] = "application/octet-stream"
payload = data
else:
- headers[CONTENT_TYPE] = "application/json"
+ headers[CONTENT_TYPE] = CONTENT_TYPE_JSON
if data is not None:
payload = json.dumps(data).encode()
else:
diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py
index 6206c67dc03504..5c5257d4181c28 100644
--- a/homeassistant/components/miflora/sensor.py
+++ b/homeassistant/components/miflora/sensor.py
@@ -17,6 +17,7 @@
CONF_NAME,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -53,7 +54,7 @@
# Sensor types are defined like: Name, units, icon
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"],
- "light": ["Light intensity", "lx", "mdi:white-balance-sunny"],
+ "light": ["Light intensity", LIGHT_LUX, "mdi:white-balance-sunny"],
"moisture": ["Moisture", PERCENTAGE, "mdi:water-percent"],
"conductivity": ["Conductivity", CONDUCTIVITY, "mdi:flash-circle"],
"battery": ["Battery", PERCENTAGE, "mdi:battery-charging"],
@@ -183,8 +184,7 @@ def available(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
- attr = {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update}
- return attr
+ return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update}
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py
index c1a41abf0d07b8..91e0f366b4dc17 100644
--- a/homeassistant/components/mikrotik/config_flow.py
+++ b/homeassistant/components/mikrotik/config_flow.py
@@ -53,8 +53,8 @@ async def async_step_user(self, user_input=None):
except CannotConnect:
errors["base"] = "cannot_connect"
except LoginError:
- errors[CONF_USERNAME] = "wrong_credentials"
- errors[CONF_PASSWORD] = "wrong_credentials"
+ errors[CONF_USERNAME] = "invalid_auth"
+ errors[CONF_PASSWORD] = "invalid_auth"
if not errors:
return self.async_create_entry(
diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json
index 9fa665add80fc3..6d421cb183827f 100644
--- a/homeassistant/components/mikrotik/strings.json
+++ b/homeassistant/components/mikrotik/strings.json
@@ -4,7 +4,7 @@
"user": {
"title": "Set up Mikrotik Router",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
@@ -15,11 +15,11 @@
},
"error": {
"name_exists": "Name exists",
- "cannot_connect": "Connection Unsuccessful",
- "wrong_credentials": "Wrong Credentials"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "Mikrotik is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/mikrotik/translations/ca.json b/homeassistant/components/mikrotik/translations/ca.json
index 10589dac474fd4..8556a3c7765f2e 100644
--- a/homeassistant/components/mikrotik/translations/ca.json
+++ b/homeassistant/components/mikrotik/translations/ca.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_configured_device": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "La connexi\u00f3 no ha tingut \u00e8xit",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"name_exists": "El nom existeix",
"wrong_credentials": "Credencials incorrectes"
},
diff --git a/homeassistant/components/mikrotik/translations/en.json b/homeassistant/components/mikrotik/translations/en.json
index 692d1247fcbf25..cad279a7afadc1 100644
--- a/homeassistant/components/mikrotik/translations/en.json
+++ b/homeassistant/components/mikrotik/translations/en.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik is already configured"
+ "already_configured": "Device is already configured",
+ "already_configured_device": "Device is already configured"
},
"error": {
- "cannot_connect": "Connection Unsuccessful",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"name_exists": "Name exists",
"wrong_credentials": "Wrong Credentials"
},
diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json
index b575db70f1156f..32897593c98da1 100644
--- a/homeassistant/components/mikrotik/translations/es.json
+++ b/homeassistant/components/mikrotik/translations/es.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik ya est\u00e1 configurado"
+ "already_configured": "Mikrotik ya est\u00e1 configurado",
+ "already_configured_device": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"cannot_connect": "Conexi\u00f3n fallida",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"name_exists": "El nombre ya existe",
"wrong_credentials": "Credenciales incorrectas"
},
diff --git a/homeassistant/components/mikrotik/translations/et.json b/homeassistant/components/mikrotik/translations/et.json
new file mode 100644
index 00000000000000..51c20225df5f6c
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/et.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json
index 0049c69632f28c..9b773d3f33600b 100644
--- a/homeassistant/components/mikrotik/translations/fr.json
+++ b/homeassistant/components/mikrotik/translations/fr.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik est d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "Mikrotik est d\u00e9j\u00e0 configur\u00e9",
+ "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de la connexion",
+ "invalid_auth": "Authentification invalide",
"name_exists": "Le nom existe",
"wrong_credentials": "Identifiants erron\u00e9s"
},
@@ -27,6 +29,7 @@
"device_tracker": {
"data": {
"arp_ping": "Activer le ping ARP",
+ "detection_time": "Intervalle de consid\u00e9ration de pr\u00e9sence",
"force_dhcp": "Forcer l'analyse \u00e0 l'aide de DHCP"
}
}
diff --git a/homeassistant/components/mikrotik/translations/it.json b/homeassistant/components/mikrotik/translations/it.json
index 104392236b22bd..a1bf16d643dae4 100644
--- a/homeassistant/components/mikrotik/translations/it.json
+++ b/homeassistant/components/mikrotik/translations/it.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Connessione Non Riuscita",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"name_exists": "Il Nome esiste gi\u00e0",
"wrong_credentials": "Credenziali Errate"
},
diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json
index 1e528fa4986a25..c5d21d5626b15c 100644
--- a/homeassistant/components/mikrotik/translations/no.json
+++ b/homeassistant/components/mikrotik/translations/no.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_configured_device": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Tilkobling mislykket",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
"name_exists": "Navnet eksisterer",
"wrong_credentials": "Feil legitimasjon"
},
diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json
index e21472d4da73f8..a4596cc70d98e0 100644
--- a/homeassistant/components/mikrotik/translations/ru.json
+++ b/homeassistant/components/mikrotik/translations/ru.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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_configured_device": "\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 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.",
"wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json
index caca14b79ed129..5285d089fc61b5 100644
--- a/homeassistant/components/mikrotik/translations/zh-Hant.json
+++ b/homeassistant/components/mikrotik/translations/zh-Hant.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u672a\u6210\u529f",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728",
"wrong_credentials": "\u6191\u8b49\u932f\u8aa4"
},
diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/mill/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json
index c9bef09227cebe..eaf1da95e9e183 100644
--- a/homeassistant/components/mill/translations/pl.json
+++ b/homeassistant/components/mill/translations/pl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"user": {
diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py
index a54fa3e2d46ba5..c9cd27d16ddbd4 100644
--- a/homeassistant/components/min_max/sensor.py
+++ b/homeassistant/components/min_max/sensor.py
@@ -228,12 +228,11 @@ def should_poll(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- state_attr = {
+ return {
attr: getattr(self, attr)
for attr in ATTR_TO_PROPERTY
if getattr(self, attr) is not None
}
- return state_attr
@property
def icon(self):
diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json
index c0a0c78d5d99f2..9e546a3cdfa171 100644
--- a/homeassistant/components/minecraft_server/strings.json
+++ b/homeassistant/components/minecraft_server/strings.json
@@ -5,7 +5,7 @@
"title": "Link your Minecraft Server",
"description": "Set up your Minecraft Server instance to allow monitoring.",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]"
}
}
@@ -16,7 +16,7 @@
"invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again."
},
"abort": {
- "already_configured": "Host is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/minecraft_server/translations/ca.json b/homeassistant/components/minecraft_server/translations/ca.json
index ce395b757135b3..b06229a1b1af26 100644
--- a/homeassistant/components/minecraft_server/translations/ca.json
+++ b/homeassistant/components/minecraft_server/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.",
diff --git a/homeassistant/components/minecraft_server/translations/en.json b/homeassistant/components/minecraft_server/translations/en.json
index fc736c667bec0a..471118773a1f2b 100644
--- a/homeassistant/components/minecraft_server/translations/en.json
+++ b/homeassistant/components/minecraft_server/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Host is already configured."
+ "already_configured": "Service is already configured"
},
"error": {
"cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.",
diff --git a/homeassistant/components/minecraft_server/translations/it.json b/homeassistant/components/minecraft_server/translations/it.json
index 7f214c4774330b..8aaee8d8b319dc 100644
--- a/homeassistant/components/minecraft_server/translations/it.json
+++ b/homeassistant/components/minecraft_server/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "L'host \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi al server. Controllare l'host e la porta e riprovare. Assicurarsi inoltre che si esegue almeno Minecraft versione 1.7 sul server.",
diff --git a/homeassistant/components/minecraft_server/translations/no.json b/homeassistant/components/minecraft_server/translations/no.json
index 4d2ecc6dbaabc2..5230401a3c3e0e 100644
--- a/homeassistant/components/minecraft_server/translations/no.json
+++ b/homeassistant/components/minecraft_server/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Verten er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"error": {
"cannot_connect": "Kan ikke koble til serveren. Kontroller verten og porten, og pr\u00f8v p\u00e5 nytt. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer minst Minecraft versjon 1.7 p\u00e5 serveren din.",
diff --git a/homeassistant/components/minecraft_server/translations/ru.json b/homeassistant/components/minecraft_server/translations/ru.json
index b95c12a1d8023e..93e182a4dfa02c 100644
--- a/homeassistant/components/minecraft_server/translations/ru.json
+++ b/homeassistant/components/minecraft_server/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d Minecraft \u0432\u0435\u0440\u0441\u0438\u0438 1.7, \u0438\u043b\u0438 \u0432\u044b\u0448\u0435.",
diff --git a/homeassistant/components/minecraft_server/translations/zh-Hant.json b/homeassistant/components/minecraft_server/translations/zh-Hant.json
index 6dc996fa67dc41..b8bd401d13e355 100644
--- a/homeassistant/components/minecraft_server/translations/zh-Hant.json
+++ b/homeassistant/components/minecraft_server/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u65bc\u4f3a\u670d\u5668\u4e0a\u57f7\u884c\u6700\u65b0\u7248\u672c Minecraft 1.7 \u7248\u3002",
diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py
index b2cb3d22e4b7f3..7c5cbd135ed454 100644
--- a/homeassistant/components/mobile_app/helpers.py
+++ b/homeassistant/components/mobile_app/helpers.py
@@ -7,7 +7,7 @@
from nacl.encoding import Base64Encoder
from nacl.secret import SecretBox
-from homeassistant.const import HTTP_BAD_REQUEST, HTTP_OK
+from homeassistant.const import CONTENT_TYPE_JSON, HTTP_BAD_REQUEST, HTTP_OK
from homeassistant.core import Context
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import HomeAssistantType
@@ -94,7 +94,7 @@ def registration_context(registration: Dict) -> Context:
def empty_okay_response(headers: Dict = None, status: int = HTTP_OK) -> Response:
"""Return a Response with empty JSON object and a 200."""
return Response(
- text="{}", status=status, content_type="application/json", headers=headers
+ text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers
)
@@ -161,7 +161,7 @@ def webhook_response(
data = json.dumps({"encrypted": True, "encrypted_data": enc_data})
return Response(
- text=data, status=status, content_type="application/json", headers=headers
+ text=data, status=status, content_type=CONTENT_TYPE_JSON, headers=headers
)
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index 62bb5fdf08da0b..04d308a5a05a82 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -12,7 +12,12 @@
ATTR_TITLE_DEFAULT,
BaseNotificationService,
)
-from homeassistant.const import HTTP_OK
+from homeassistant.const import (
+ HTTP_ACCEPTED,
+ HTTP_CREATED,
+ HTTP_OK,
+ HTTP_TOO_MANY_REQUESTS,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.util.dt as dt_util
@@ -135,7 +140,7 @@ async def async_send_message(self, message="", **kwargs):
response = await self._session.post(push_url, json=data)
result = await response.json()
- if response.status in [HTTP_OK, 201, 202]:
+ if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]:
log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME], result)
continue
@@ -152,7 +157,7 @@ async def async_send_message(self, message="", **kwargs):
" This message is generated externally to Home Assistant."
)
- if response.status == 429:
+ if response.status == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(message)
log_rate_limits(
self.hass, entry_data[ATTR_DEVICE_NAME], result, logging.WARNING
diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json
new file mode 100644
index 00000000000000..e5c01546976167
--- /dev/null
+++ b/homeassistant/components/mobile_app/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "install_app": "Home Assistantiga sidumiseks avage mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kas soovid seadistada mobiilirakenduse sidumist?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py
index 2f5e69fd02b446..bbeea2e9521231 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -26,6 +26,7 @@
ATTR_DOMAIN,
ATTR_SERVICE,
ATTR_SERVICE_DATA,
+ ATTR_SUPPORTED_FEATURES,
CONF_WEBHOOK_ID,
HTTP_BAD_REQUEST,
HTTP_CREATED,
@@ -267,7 +268,7 @@ async def webhook_stream_camera(hass, config_entry, data):
resp = {"mjpeg_path": "/api/camera_proxy_stream/%s" % (camera.entity_id)}
- if camera.attributes["supported_features"] & CAMERA_SUPPORT_STREAM:
+ if camera.attributes[ATTR_SUPPORTED_FEATURES] & CAMERA_SUPPORT_STREAM:
try:
resp["hls_path"] = await hass.components.camera.async_request_stream(
camera.entity_id, "hls"
diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 0a7ea08543ab77..822000cb56a023 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -6,29 +6,50 @@
from pymodbus.transaction import ModbusRtuFramer
import voluptuous as vol
+from homeassistant.components.cover import (
+ DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
+)
from homeassistant.const import (
ATTR_STATE,
+ CONF_COVERS,
CONF_DELAY,
+ CONF_DEVICE_CLASS,
CONF_HOST,
CONF_METHOD,
CONF_NAME,
CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_SLAVE,
CONF_TIMEOUT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import load_platform
from .const import (
ATTR_ADDRESS,
ATTR_HUB,
ATTR_UNIT,
ATTR_VALUE,
+ CALL_TYPE_COIL,
+ CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_REGISTER_INPUT,
CONF_BAUDRATE,
CONF_BYTESIZE,
+ CONF_INPUT_TYPE,
CONF_PARITY,
+ CONF_REGISTER,
+ CONF_STATE_CLOSED,
+ CONF_STATE_CLOSING,
+ CONF_STATE_OPEN,
+ CONF_STATE_OPENING,
+ CONF_STATUS_REGISTER,
+ CONF_STATUS_REGISTER_TYPE,
CONF_STOPBITS,
DEFAULT_HUB,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_SLAVE,
MODBUS_DOMAIN as DOMAIN,
SERVICE_WRITE_COIL,
SERVICE_WRITE_REGISTER,
@@ -36,9 +57,33 @@
_LOGGER = logging.getLogger(__name__)
-
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
+COVERS_SCHEMA = vol.All(
+ cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER),
+ vol.Schema(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(
+ cv.time_period, lambda value: value.total_seconds()
+ ),
+ vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_SLAVE, default=DEFAULT_SLAVE): cv.positive_int,
+ vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
+ vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
+ vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
+ vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
+ vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
+ vol.Optional(
+ CONF_STATUS_REGISTER_TYPE,
+ default=CALL_TYPE_REGISTER_HOLDING,
+ ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
+ vol.Exclusive(CALL_TYPE_COIL, CONF_INPUT_TYPE): cv.positive_int,
+ vol.Exclusive(CONF_REGISTER, CONF_INPUT_TYPE): cv.positive_int,
+ }
+ ),
+)
+
SERIAL_SCHEMA = BASE_SCHEMA.extend(
{
vol.Required(CONF_BAUDRATE): cv.positive_int,
@@ -49,6 +94,7 @@
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
vol.Required(CONF_TYPE): "serial",
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
+ vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
}
)
@@ -59,14 +105,10 @@
vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
+ vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
}
)
-CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])},
- extra=vol.ALLOW_EXTRA,
-)
-
SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
@@ -87,13 +129,30 @@
}
)
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.All(
+ cv.ensure_list,
+ [
+ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
+ ],
+ ),
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
def setup(hass, config):
"""Set up Modbus component."""
hass.data[DOMAIN] = hub_collect = {}
- for client_config in config[DOMAIN]:
- hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config)
+ for conf_hub in config[DOMAIN]:
+ hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub)
+
+ # load platforms
+ for component, conf_key in (("cover", CONF_COVERS),):
+ if conf_key in conf_hub:
+ load_platform(hass, component, DOMAIN, conf_hub, config)
def stop_modbus(event):
"""Stop Modbus service."""
diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py
index c12c50cdc075be..dc29dd626ae966 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -46,6 +46,7 @@
ATTR_VALUE = "value"
SERVICE_WRITE_COIL = "write_coil"
SERVICE_WRITE_REGISTER = "write_register"
+DEFAULT_SCAN_INTERVAL = 15 # seconds
# binary_sensor.py
CONF_INPUTS = "inputs"
@@ -71,3 +72,12 @@
CONF_MAX_TEMP = "max_temp"
CONF_MIN_TEMP = "min_temp"
CONF_STEP = "temp_step"
+
+# cover.py
+CONF_STATE_OPEN = "state_open"
+CONF_STATE_CLOSED = "state_closed"
+CONF_STATE_OPENING = "state_opening"
+CONF_STATE_CLOSING = "state_closing"
+CONF_STATUS_REGISTER = "status_register"
+CONF_STATUS_REGISTER_TYPE = "status_register_type"
+DEFAULT_SLAVE = 1
diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py
new file mode 100644
index 00000000000000..a7c9c301ac55a9
--- /dev/null
+++ b/homeassistant/components/modbus/cover.py
@@ -0,0 +1,244 @@
+"""Support for Modbus covers."""
+from datetime import timedelta
+import logging
+from typing import Any, Dict, Optional
+
+from pymodbus.exceptions import ConnectionException, ModbusException
+from pymodbus.pdu import ExceptionResponse
+
+from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity
+from homeassistant.const import (
+ CONF_COVERS,
+ CONF_DEVICE_CLASS,
+ CONF_NAME,
+ CONF_SCAN_INTERVAL,
+ CONF_SLAVE,
+)
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import (
+ ConfigType,
+ DiscoveryInfoType,
+ HomeAssistantType,
+)
+
+from . import ModbusHub
+from .const import (
+ CALL_TYPE_COIL,
+ CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_REGISTER_INPUT,
+ CONF_REGISTER,
+ CONF_STATE_CLOSED,
+ CONF_STATE_CLOSING,
+ CONF_STATE_OPEN,
+ CONF_STATE_OPENING,
+ CONF_STATUS_REGISTER,
+ CONF_STATUS_REGISTER_TYPE,
+ MODBUS_DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ async_add_entities,
+ discovery_info: Optional[DiscoveryInfoType] = None,
+):
+ """Read configuration and create Modbus cover."""
+ if discovery_info is None:
+ return
+
+ covers = []
+ for cover in discovery_info[CONF_COVERS]:
+ hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
+ covers.append(ModbusCover(hub, cover))
+
+ async_add_entities(covers)
+
+
+class ModbusCover(CoverEntity, RestoreEntity):
+ """Representation of a Modbus cover."""
+
+ def __init__(
+ self,
+ hub: ModbusHub,
+ config: Dict[str, Any],
+ ):
+ """Initialize the modbus cover."""
+ self._hub: ModbusHub = hub
+ self._coil = config.get(CALL_TYPE_COIL)
+ self._device_class = config.get(CONF_DEVICE_CLASS)
+ self._name = config[CONF_NAME]
+ self._register = config.get(CONF_REGISTER)
+ self._slave = config[CONF_SLAVE]
+ self._state_closed = config[CONF_STATE_CLOSED]
+ self._state_closing = config[CONF_STATE_CLOSING]
+ self._state_open = config[CONF_STATE_OPEN]
+ self._state_opening = config[CONF_STATE_OPENING]
+ self._status_register = config.get(CONF_STATUS_REGISTER)
+ self._status_register_type = config[CONF_STATUS_REGISTER_TYPE]
+ self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL])
+ self._value = None
+ self._available = True
+
+ # If we read cover status from coil, and not from optional status register,
+ # we interpret boolean value False as closed cover, and value True as open cover.
+ # Intermediate states are not supported in such a setup.
+ if self._coil is not None and self._status_register is None:
+ self._state_closed = False
+ self._state_open = True
+ self._state_closing = None
+ self._state_opening = None
+
+ # If we read cover status from the main register (i.e., an optional
+ # status register is not specified), we need to make sure the register_type
+ # is set to "holding".
+ if self._register is not None and self._status_register is None:
+ self._status_register = self._register
+ self._status_register_type = CALL_TYPE_REGISTER_HOLDING
+
+ async def async_added_to_hass(self):
+ """Handle entity which will be added."""
+ state = await self.async_get_last_state()
+ if not state:
+ return
+ self._value = state.state
+
+ async_track_time_interval(
+ self.hass, lambda arg: self._update(), self._scan_interval
+ )
+
+ @property
+ def device_class(self) -> Optional[str]:
+ """Return the device class of the sensor."""
+ return self._device_class
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ return self._name
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_OPEN | SUPPORT_CLOSE
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ @property
+ def is_opening(self):
+ """Return if the cover is opening or not."""
+ return self._value == self._state_opening
+
+ @property
+ def is_closing(self):
+ """Return if the cover is closing or not."""
+ return self._value == self._state_closing
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ return self._value == self._state_closed
+
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+
+ # Handle polling directly in this entity
+ return False
+
+ def open_cover(self, **kwargs: Any) -> None:
+ """Open cover."""
+ if self._coil is not None:
+ self._write_coil(True)
+ else:
+ self._write_register(self._state_open)
+
+ self._update()
+
+ def close_cover(self, **kwargs: Any) -> None:
+ """Close cover."""
+ if self._coil is not None:
+ self._write_coil(False)
+ else:
+ self._write_register(self._state_closed)
+
+ self._update()
+
+ def _update(self):
+ """Update the state of the cover."""
+ if self._coil is not None and self._status_register is None:
+ self._value = self._read_coil()
+ else:
+ self._value = self._read_status_register()
+
+ self.schedule_update_ha_state()
+
+ def _read_status_register(self) -> Optional[int]:
+ """Read status register using the Modbus hub slave."""
+ try:
+ if self._status_register_type == CALL_TYPE_REGISTER_INPUT:
+ result = self._hub.read_input_registers(
+ self._slave, self._status_register, 1
+ )
+ else:
+ result = self._hub.read_holding_registers(
+ self._slave, self._status_register, 1
+ )
+ except ConnectionException:
+ self._available = False
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._available = False
+ return
+
+ value = int(result.registers[0])
+ self._available = True
+
+ return value
+
+ def _write_register(self, value):
+ """Write holding register using the Modbus hub slave."""
+ try:
+ self._hub.write_register(self._slave, self._register, value)
+ except ConnectionException:
+ self._available = False
+ return
+
+ self._available = True
+
+ def _read_coil(self) -> Optional[bool]:
+ """Read coil using the Modbus hub slave."""
+ try:
+ result = self._hub.read_coils(self._slave, self._coil, 1)
+ except ConnectionException:
+ self._available = False
+ return
+
+ if isinstance(result, (ModbusException, ExceptionResponse)):
+ self._available = False
+ return
+
+ value = bool(result.bits[0])
+ self._available = True
+
+ return value
+
+ def _write_coil(self, value):
+ """Write coil using the Modbus hub slave."""
+ try:
+ self._hub.write_coil(self._slave, self._coil, value)
+ except ConnectionException:
+ self._available = False
+ return
+
+ self._available = True
diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json
index a9155c7b628d3f..05e9c39c4b5b80 100644
--- a/homeassistant/components/modbus/manifest.json
+++ b/homeassistant/components/modbus/manifest.json
@@ -3,5 +3,5 @@
"name": "Modbus",
"documentation": "https://www.home-assistant.io/integrations/modbus",
"requirements": ["pymodbus==2.3.0"],
- "codeowners": ["@adamchengtkc", "@janiversen"]
+ "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"]
}
diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py
index 8037d926ef1f1f..fa5b42807b0a8f 100644
--- a/homeassistant/components/modbus/switch.py
+++ b/homeassistant/components/modbus/switch.py
@@ -158,19 +158,22 @@ def update(self):
"""Update the state of the switch."""
self._is_on = self._read_coil(self._coil)
- def _read_coil(self, coil) -> Optional[bool]:
+ def _read_coil(self, coil) -> bool:
"""Read coil using the Modbus hub slave."""
try:
result = self._hub.read_coils(self._slave, coil, 1)
except ConnectionException:
self._available = False
- return
+ return False
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
- return
+ return False
self._available = True
+ # bits[0] select the lowest bit in result,
+ # is_on for a binary_sensor is true if the bit are 1
+ # The other bits are not considered.
return bool(result.bits[0])
def _write_coil(self, coil, value):
diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json
index c25fb901d7658c..008c182f41b063 100644
--- a/homeassistant/components/monoprice/strings.json
+++ b/homeassistant/components/monoprice/strings.json
@@ -15,11 +15,11 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -37,4 +37,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/monoprice/translations/ca.json b/homeassistant/components/monoprice/translations/ca.json
index 6af5204b91e15e..1e4c623c215649 100644
--- a/homeassistant/components/monoprice/translations/ca.json
+++ b/homeassistant/components/monoprice/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/en.json b/homeassistant/components/monoprice/translations/en.json
index 9e9f3a4d2cfb27..08438f8a985ee5 100644
--- a/homeassistant/components/monoprice/translations/en.json
+++ b/homeassistant/components/monoprice/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/it.json b/homeassistant/components/monoprice/translations/it.json
index b89758a9da3598..d084929e320321 100644
--- a/homeassistant/components/monoprice/translations/it.json
+++ b/homeassistant/components/monoprice/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/no.json b/homeassistant/components/monoprice/translations/no.json
index acd4bde8774102..93efecbf54aea4 100644
--- a/homeassistant/components/monoprice/translations/no.json
+++ b/homeassistant/components/monoprice/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/pl.json b/homeassistant/components/monoprice/translations/pl.json
index b5af0e8851f9de..020e0f2c554a76 100644
--- a/homeassistant/components/monoprice/translations/pl.json
+++ b/homeassistant/components/monoprice/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/monoprice/translations/ru.json b/homeassistant/components/monoprice/translations/ru.json
index 5b891db80a38c3..4fb5eb892a1de9 100644
--- a/homeassistant/components/monoprice/translations/ru.json
+++ b/homeassistant/components/monoprice/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json
index ca1923d62d6cab..e653bda9205709 100644
--- a/homeassistant/components/monoprice/translations/zh-Hant.json
+++ b/homeassistant/components/monoprice/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
diff --git a/homeassistant/components/moon/translations/sensor.cs.json b/homeassistant/components/moon/translations/sensor.cs.json
index ff430618ff96eb..368fa833ea023f 100644
--- a/homeassistant/components/moon/translations/sensor.cs.json
+++ b/homeassistant/components/moon/translations/sensor.cs.json
@@ -3,8 +3,12 @@
"moon__phase": {
"first_quarter": "Prvn\u00ed \u010dtvr\u0165",
"full_moon": "\u00dapln\u011bk",
- "waxing_crescent": "Dor\u016fstaj\u00edc\u00ed srpek",
- "waxing_gibbous": "Prvn\u00ed \u010dtvr\u0165"
+ "last_quarter": "Posledn\u00ed \u010dtvrt",
+ "new_moon": "Nov",
+ "waning_crescent": "Ub\u00fdvaj\u00edc\u00ed p\u016flm\u011bs\u00edc",
+ "waning_gibbous": "Ub\u00fdvaj\u00edc\u00ed m\u011bs\u00edc",
+ "waxing_crescent": "Dor\u016fstaj\u00edc\u00ed p\u016flm\u011bs\u00edc",
+ "waxing_gibbous": "Dor\u016fstaj\u00edc\u00ed m\u011bs\u00edc"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/moon/translations/sensor.et.json b/homeassistant/components/moon/translations/sensor.et.json
new file mode 100644
index 00000000000000..e147772786848c
--- /dev/null
+++ b/homeassistant/components/moon/translations/sensor.et.json
@@ -0,0 +1,14 @@
+{
+ "state": {
+ "moon__phase": {
+ "first_quarter": "Kasvav poolkuu",
+ "full_moon": "T\u00e4iskuu",
+ "last_quarter": "Kahanev poolkuu",
+ "new_moon": "Kuu loomine",
+ "waning_crescent": "Vanakuu",
+ "waning_gibbous": "Kahanev kuu",
+ "waxing_crescent": "Noorkuu",
+ "waxing_gibbous": "Kasvav kuu"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py
index 8a46cef6eb35d7..845b0ae506ba71 100644
--- a/homeassistant/components/mpd/media_player.py
+++ b/homeassistant/components/mpd/media_player.py
@@ -138,12 +138,12 @@ def _fetch_status(self):
if position is None:
position = self._status.get("time")
- if position is not None and ":" in position:
+ if isinstance(position, str) and ":" in position:
position = position.split(":")[0]
if position is not None and self._media_position != position:
self._media_position_updated_at = dt_util.utcnow()
- self._media_position = int(position)
+ self._media_position = int(float(position))
self._update_playlists()
@@ -159,8 +159,9 @@ def update(self):
self._connect()
self._fetch_status()
- except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError):
+ except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error:
# Cleanly disconnect in case connection is not in valid state
+ _LOGGER.debug("Error updating status: %s", error)
self._disconnect()
@property
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 2b5dca6474f812..b42032c6a1c8a2 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -60,6 +60,7 @@
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_WILL_MESSAGE,
+ DATA_MQTT_CONFIG,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_PAYLOAD_AVAILABLE,
@@ -88,7 +89,6 @@
DOMAIN = "mqtt"
DATA_MQTT = "mqtt"
-DATA_MQTT_CONFIG = "mqtt_config"
SERVICE_PUBLISH = "publish"
SERVICE_DUMP = "dump"
@@ -134,7 +134,7 @@
CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable"
DISCOVERY_COOLDOWN = 2
-TIMEOUT_ACK = 1
+TIMEOUT_ACK = 10
PLATFORMS = [
"alarm_control_panel",
@@ -630,6 +630,7 @@ class Subscription:
"""Class to hold data about an active subscription."""
topic: str = attr.ib()
+ matcher: Any = attr.ib()
callback: MessageCallbackType = attr.ib()
qos: int = attr.ib(default=0)
encoding: str = attr.ib(default="utf-8")
@@ -838,7 +839,9 @@ async def async_subscribe(
if not isinstance(topic, str):
raise HomeAssistantError("Topic needs to be a string!")
- subscription = Subscription(topic, msg_callback, qos, encoding)
+ subscription = Subscription(
+ topic, _matcher_for_topic(topic), msg_callback, qos, encoding
+ )
self.subscriptions.append(subscription)
# Only subscribe if currently connected.
@@ -953,7 +956,7 @@ def _mqtt_handle_message(self, msg) -> None:
timestamp = dt_util.utcnow()
for subscription in self.subscriptions:
- if not _match_topic(subscription.topic, msg.topic):
+ if not subscription.matcher(msg.topic):
continue
payload: SubscribePayloadType = msg.payload
@@ -1050,18 +1053,14 @@ def _raise_on_error(result_code: int) -> None:
)
-def _match_topic(subscription: str, topic: str) -> bool:
- """Test if topic matches subscription."""
+def _matcher_for_topic(subscription: str) -> Any:
# pylint: disable=import-outside-toplevel
from paho.mqtt.matcher import MQTTMatcher
matcher = MQTTMatcher()
matcher[subscription] = True
- try:
- next(matcher.iter_match(topic))
- return True
- except StopIteration:
- return False
+
+ return lambda topic: next(matcher.iter_match(topic), False)
class MqttAttributes(Entity):
@@ -1229,7 +1228,7 @@ async def cleanup_device_registry(hass, device_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
+ from . import device_trigger, tag
device_registry = await hass.helpers.device_registry.async_get_registry()
entity_registry = await hass.helpers.entity_registry.async_get_registry()
@@ -1239,6 +1238,7 @@ async def cleanup_device_registry(hass, device_id):
entity_registry, device_id
)
and not await device_trigger.async_get_triggers(hass, device_id)
+ and not tag.async_has_tags(hass, device_id)
):
device_registry.async_remove_device(device_id)
@@ -1465,3 +1465,33 @@ async def forward_messages(mqttmsg: Message):
)
connection.send_message(websocket_api.result_message(msg["id"]))
+
+
+@callback
+def async_subscribe_connection_status(hass, connection_status_callback):
+ """Subscribe to MQTT connection changes."""
+
+ @callback
+ def connected():
+ hass.async_add_job(connection_status_callback, True)
+
+ @callback
+ def disconnected():
+ _LOGGER.error("Calling connection_status_callback, False")
+ hass.async_add_job(connection_status_callback, False)
+
+ subscriptions = {
+ "connect": async_dispatcher_connect(hass, MQTT_CONNECTED, connected),
+ "disconnect": async_dispatcher_connect(hass, MQTT_DISCONNECTED, disconnected),
+ }
+
+ def unsubscribe():
+ subscriptions["connect"]()
+ subscriptions["disconnect"]()
+
+ return unsubscribe
+
+
+def is_connected(hass):
+ """Return if MQTT client is connected."""
+ return hass.data[DATA_MQTT].connected
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index 8b1c350323c05b..5c4016437a62d2 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -24,6 +24,7 @@
CONF_BROKER,
CONF_DISCOVERY,
CONF_WILL_MESSAGE,
+ DATA_MQTT_CONFIG,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_WILL,
@@ -162,6 +163,7 @@ async def async_step_broker(self, user_input=None):
"""Manage the MQTT options."""
errors = {}
current_config = self.config_entry.data
+ yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
@@ -178,20 +180,22 @@ async def async_step_broker(self, user_input=None):
errors["base"] = "cannot_connect"
fields = OrderedDict()
- fields[vol.Required(CONF_BROKER, default=current_config[CONF_BROKER])] = str
- fields[vol.Required(CONF_PORT, default=current_config[CONF_PORT])] = vol.Coerce(
- int
- )
+ current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER))
+ current_port = current_config.get(CONF_PORT, yaml_config.get(CONF_PORT))
+ current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME))
+ current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD))
+ fields[vol.Required(CONF_BROKER, default=current_broker)] = str
+ fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int)
fields[
vol.Optional(
CONF_USERNAME,
- description={"suggested_value": current_config.get(CONF_USERNAME)},
+ description={"suggested_value": current_user},
)
] = str
fields[
vol.Optional(
CONF_PASSWORD,
- description={"suggested_value": current_config.get(CONF_PASSWORD)},
+ description={"suggested_value": current_pass},
)
] = str
@@ -205,6 +209,7 @@ async def async_step_options(self, user_input=None):
"""Manage the MQTT options."""
errors = {}
current_config = self.config_entry.data
+ yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
options_config = {}
if user_input is not None:
bad_birth = False
@@ -253,16 +258,24 @@ async def async_step_options(self, user_input=None):
)
return self.async_create_entry(title="", data=None)
- birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})}
- will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})}
+ birth = {
+ **DEFAULT_BIRTH,
+ **current_config.get(
+ CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {})
+ ),
+ }
+ will = {
+ **DEFAULT_WILL,
+ **current_config.get(
+ CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {})
+ ),
+ }
+ discovery = current_config.get(
+ CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
+ )
fields = OrderedDict()
- fields[
- vol.Optional(
- CONF_DISCOVERY,
- default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY),
- )
- ] = bool
+ fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool
# Birth message is disabled if CONF_BIRTH_MESSAGE = {}
fields[
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index 7ea6d9d348bc4e..5ab3f756311e0e 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -17,6 +17,8 @@
CONF_STATE_TOPIC = "state_topic"
CONF_WILL_MESSAGE = "will_message"
+DATA_MQTT_CONFIG = "mqtt_config"
+
DEFAULT_PREFIX = "homeassistant"
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
DEFAULT_DISCOVERY = False
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index a7d5236148b997..6c4cbfd212fefd 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -32,6 +32,7 @@
"lock",
"sensor",
"switch",
+ "tag",
"vacuum",
]
@@ -154,6 +155,12 @@ async def async_device_message_received(msg):
from . import device_automation
await device_automation.async_setup_entry(hass, config_entry)
+ elif component == "tag":
+ # Local import to avoid circular dependencies
+ # pylint: disable=import-outside-toplevel
+ from . import tag
+
+ await tag.async_setup_entry(hass, config_entry)
else:
await hass.config_entries.async_forward_entry_setup(
config_entry, component
diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json
index 8b293eb06f6585..4d44090a4e3559 100644
--- a/homeassistant/components/mqtt/manifest.json
+++ b/homeassistant/components/mqtt/manifest.json
@@ -3,7 +3,7 @@
"name": "MQTT",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mqtt",
- "requirements": ["paho-mqtt==1.5.0"],
+ "requirements": ["paho-mqtt==1.5.1"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core", "@emontnemery"]
}
diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json
index 75c3fdec2607ce..8c3db8e5b61eb0 100644
--- a/homeassistant/components/mqtt/strings.json
+++ b/homeassistant/components/mqtt/strings.json
@@ -20,10 +20,10 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of MQTT is allowed."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {
- "cannot_connect": "Unable to connect to the broker."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"device_automation": {
@@ -68,7 +68,7 @@
"birth_payload": "Birth message payload",
"birth_qos": "Birth message QoS",
"birth_retain": "Birth message retain",
- "will_enable": "Enable birth message",
+ "will_enable": "Enable will message",
"will_topic": "Will message topic",
"will_payload": "Will message payload",
"will_qos": "Will message QoS",
@@ -77,9 +77,9 @@
}
},
"error": {
- "cannot_connect": "Unable to connect to the broker.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"bad_birth": "Invalid birth topic.",
"bad_will": "Invalid will topic."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py
new file mode 100644
index 00000000000000..94356ccf778c31
--- /dev/null
+++ b/homeassistant/components/mqtt/tag.py
@@ -0,0 +1,224 @@
+"""Provides tag scanning for MQTT."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components import mqtt
+from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE
+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
+
+from . import (
+ ATTR_DISCOVERY_HASH,
+ ATTR_DISCOVERY_TOPIC,
+ CONF_CONNECTIONS,
+ CONF_DEVICE,
+ CONF_IDENTIFIERS,
+ CONF_QOS,
+ CONF_TOPIC,
+ DOMAIN,
+ cleanup_device_registry,
+ subscription,
+)
+from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash
+from .util import valid_subscribe_topic
+
+_LOGGER = logging.getLogger(__name__)
+
+TAG = "tag"
+TAGS = "mqtt_tags"
+
+PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
+ vol.Optional(CONF_PLATFORM): "mqtt",
+ vol.Required(CONF_TOPIC): valid_subscribe_topic,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ },
+ mqtt.validate_device_has_at_least_one_identifier,
+)
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up MQTT tag scan dynamically through MQTT discovery."""
+
+ async def async_discover(discovery_payload):
+ """Discover and add MQTT tag scan."""
+ discovery_data = discovery_payload.discovery_data
+ try:
+ config = PLATFORM_SCHEMA(discovery_payload)
+ await async_setup_tag(hass, config, config_entry, discovery_data)
+ except Exception:
+ clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
+ raise
+
+ async_dispatcher_connect(
+ hass, MQTT_DISCOVERY_NEW.format("tag", "mqtt"), async_discover
+ )
+
+
+async def async_setup_tag(hass, config, config_entry, discovery_data):
+ """Set up the MQTT tag scanner."""
+ discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
+ discovery_id = discovery_hash[1]
+
+ device_id = None
+ if CONF_DEVICE in config:
+ await _update_device(hass, config_entry, config)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(
+ {(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]},
+ {tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]},
+ )
+
+ if device is None:
+ return
+ device_id = device.id
+
+ if TAGS not in hass.data:
+ hass.data[TAGS] = {}
+ if device_id not in hass.data[TAGS]:
+ hass.data[TAGS][device_id] = {}
+
+ tag_scanner = MQTTTagScanner(
+ hass,
+ config,
+ device_id,
+ discovery_data,
+ config_entry,
+ )
+
+ await tag_scanner.setup()
+
+ if device_id:
+ hass.data[TAGS][device_id][discovery_id] = tag_scanner
+
+
+def async_has_tags(hass, device_id):
+ """Device has tag scanners."""
+ if TAGS not in hass.data or device_id not in hass.data[TAGS]:
+ return False
+ return hass.data[TAGS][device_id] != {}
+
+
+class MQTTTagScanner:
+ """MQTT Tag scanner."""
+
+ def __init__(self, hass, config, device_id, discovery_data, config_entry):
+ """Initialize."""
+ self._config = config
+ self._config_entry = config_entry
+ self.device_id = device_id
+ self.discovery_data = discovery_data
+ self.hass = hass
+ self._remove_discovery = None
+ self._remove_device_updated = None
+ self._sub_state = None
+ self._value_template = None
+
+ self._setup_from_config(config)
+
+ async def discovery_update(self, payload):
+ """Handle discovery update."""
+ discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
+ _LOGGER.info(
+ "Got update for tag scanner with hash: %s '%s'", discovery_hash, payload
+ )
+ if not payload:
+ # Empty payload: Remove tag scanner
+ _LOGGER.info("Removing tag scanner: %s", discovery_hash)
+ await self.tear_down()
+ if self.device_id:
+ await cleanup_device_registry(self.hass, self.device_id)
+ else:
+ # Non-empty payload: Update tag scanner
+ _LOGGER.info("Updating tag scanner: %s", discovery_hash)
+ config = PLATFORM_SCHEMA(payload)
+ self._config = config
+ if self.device_id:
+ await _update_device(self.hass, self._config_entry, config)
+ self._setup_from_config(config)
+ await self.subscribe_topics()
+
+ def _setup_from_config(self, config):
+ self._value_template = lambda value, error_value: value
+ if CONF_VALUE_TEMPLATE in config:
+ value_template = config.get(CONF_VALUE_TEMPLATE)
+ value_template.hass = self.hass
+
+ self._value_template = value_template.async_render_with_possible_json_value
+
+ async def setup(self):
+ """Set up the MQTT tag scanner."""
+ discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
+ await self.subscribe_topics()
+ if self.device_id:
+ self._remove_device_updated = self.hass.bus.async_listen(
+ EVENT_DEVICE_REGISTRY_UPDATED, self.device_removed
+ )
+ self._remove_discovery = async_dispatcher_connect(
+ self.hass,
+ MQTT_DISCOVERY_UPDATED.format(discovery_hash),
+ self.discovery_update,
+ )
+
+ async def subscribe_topics(self):
+ """Subscribe to MQTT topics."""
+
+ async def tag_scanned(msg):
+ tag_id = self._value_template(msg.payload, error_value="").strip()
+ if not tag_id: # No output from template, ignore
+ return
+
+ await self.hass.components.tag.async_scan_tag(tag_id, self.device_id)
+
+ self._sub_state = await subscription.async_subscribe_topics(
+ self.hass,
+ self._sub_state,
+ {
+ "state_topic": {
+ "topic": self._config[CONF_TOPIC],
+ "msg_callback": tag_scanned,
+ "qos": self._config[CONF_QOS],
+ }
+ },
+ )
+
+ async def device_removed(self, event):
+ """Handle the removal of a device."""
+ device_id = event.data["device_id"]
+ if event.data["action"] != "remove" or device_id != self.device_id:
+ return
+
+ await self.tear_down()
+
+ async def tear_down(self):
+ """Cleanup tag scanner."""
+ discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
+ discovery_id = discovery_hash[1]
+ discovery_topic = self.discovery_data[ATTR_DISCOVERY_TOPIC]
+
+ clear_discovery_hash(self.hass, discovery_hash)
+ if self.device_id:
+ self._remove_device_updated()
+ self._remove_discovery()
+
+ mqtt.publish(self.hass, discovery_topic, "", retain=True)
+ self._sub_state = await subscription.async_unsubscribe_topics(
+ self.hass, self._sub_state
+ )
+ if self.device_id:
+ self.hass.data[TAGS][self.device_id].pop(discovery_id)
+
+
+async def _update_device(hass, config_entry, config):
+ """Update device registry."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ config_entry_id = config_entry.entry_id
+ device_info = mqtt.device_info_from_config(config[CONF_DEVICE])
+
+ if config_entry_id is not None and device_info is not None:
+ device_info["config_entry_id"] = config_entry_id
+ device_registry.async_get_or_create(**device_info)
diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json
index e94be2d1a6d568..f72ee30cdcfc3a 100644
--- a/homeassistant/components/mqtt/translations/ca.json
+++ b/homeassistant/components/mqtt/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de MQTT."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
- "cannot_connect": "No s'ha pogut connectar amb el broker."
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "Topic missatge de naixement inv\u00e0lid.",
"bad_will": "Topic missatge d'\u00faltima voluntat inv\u00e0lid.",
- "cannot_connect": "No es pot connectar amb el broker."
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "Retenci\u00f3 missatge de naixement",
"birth_topic": "Topic missatge de naixement",
"discovery": "Activar descobriment",
- "will_enable": "Activa el missatge de naixement",
+ "will_enable": "Activa el missatge d'\u00faltima voluntat",
"will_payload": "Dades (payload) missatge d'\u00faltima voluntat",
"will_qos": "QoS missatge d'\u00faltima voluntat",
"will_retain": "Retenci\u00f3 missatge d'\u00faltima voluntat",
diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json
index 7256fe2f956cd5..7f34e10fa896c4 100644
--- a/homeassistant/components/mqtt/translations/de.json
+++ b/homeassistant/components/mqtt/translations/de.json
@@ -52,7 +52,10 @@
"step": {
"broker": {
"data": {
- "password": "Passwort"
+ "broker": "Broker",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
}
}
}
diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json
index 8ece91cb85d796..362e51b440537d 100644
--- a/homeassistant/components/mqtt/translations/en.json
+++ b/homeassistant/components/mqtt/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Only a single configuration of MQTT is allowed."
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
- "cannot_connect": "Unable to connect to the broker."
+ "cannot_connect": "Failed to connect"
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "Invalid birth topic.",
"bad_will": "Invalid will topic.",
- "cannot_connect": "Unable to connect to the broker."
+ "cannot_connect": "Failed to connect"
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "Birth message retain",
"birth_topic": "Birth message topic",
"discovery": "Enable discovery",
- "will_enable": "Enable birth message",
+ "will_enable": "Enable will message",
"will_payload": "Will message payload",
"will_qos": "Will message QoS",
"will_retain": "Will message retain",
diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json
new file mode 100644
index 00000000000000..e8a9fac81d7416
--- /dev/null
+++ b/homeassistant/components/mqtt/translations/et.json
@@ -0,0 +1,85 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Lubatud on ainult \u00fcks MQTT konfiguratsioon."
+ },
+ "error": {
+ "cannot_connect": "Vahendajaga ei saa \u00fchendust luua."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Vahendaja",
+ "discovery": "Luba automaatne avastamine",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisestage oma MQTT vahendaja andmed."
+ },
+ "hassio_confirm": {
+ "data": {
+ "discovery": "Luba automaatne avastamine"
+ },
+ "description": "Kas soovite seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?",
+ "title": "MQTT vahendaja Hass.io pistikprogrammi kaudu"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Esimene nupp",
+ "button_2": "Teine nupp",
+ "button_3": "Kolmas nupp",
+ "button_4": "Neljas nupp",
+ "button_5": "Viies nupp",
+ "button_6": "Kuues nupp",
+ "turn_off": "L\u00fclita v\u00e4lja",
+ "turn_on": "L\u00fclita sisse"
+ },
+ "trigger_type": {
+ "button_double_press": "\" {subtype} \" on topeltkl\u00f5psatud",
+ "button_long_press": "\" {subtype} \" on pikalt alla vajutatud",
+ "button_long_release": "\"{subtype}\" vabastatati p\u00e4rast pikka vajutust",
+ "button_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud",
+ "button_quintuple_press": "\"{subtype}\" on viiekordselt kl\u00f5psatud",
+ "button_short_press": "\u201e {subtype} \u201d on vajutatud",
+ "button_short_release": "\" {subtype} \" vabastati",
+ "button_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud"
+ }
+ },
+ "options": {
+ "error": {
+ "bad_birth": "Kehtetu loomise teavitus.",
+ "bad_will": "Kehtetu l\u00f5petamise teavitus.",
+ "cannot_connect": "Vahendajaga ei saa \u00fchendust luua."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Vahendaja",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisestage oma MQTT vahendaja \u00fchenduse teave."
+ },
+ "options": {
+ "data": {
+ "birth_enable": "Luba loomisteavitus",
+ "birth_payload": "S\u00fcnniteate v\u00e4\u00e4rtus",
+ "birth_qos": "S\u00fcnniteate QoS",
+ "birth_retain": "S\u00fcnniteate j\u00e4\u00e4dvustamine",
+ "birth_topic": "S\u00fcnniteate teema",
+ "discovery": "Luba avastamine",
+ "will_enable": "Luba loomisteavitus",
+ "will_payload": "L\u00f5petamisteate v\u00e4\u00e4rtus",
+ "will_qos": "L\u00f5petamisteate QoS",
+ "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine",
+ "will_topic": "L\u00f5petamisteade"
+ },
+ "description": "Valige MQTT s\u00e4tted."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json
index 3488c32391149e..845d0efabc787a 100644
--- a/homeassistant/components/mqtt/translations/it.json
+++ b/homeassistant/components/mqtt/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di MQTT."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
- "cannot_connect": "Impossibile connettersi al broker."
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "Argomento birth non valido.",
"bad_will": "Argomento will non valido.",
- "cannot_connect": "Impossibile connettersi al broker."
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "Persistenza del messaggio birth",
"birth_topic": "Argomento del messaggio birth",
"discovery": "Attiva l'individuazione",
- "will_enable": "Abilita messaggio di nascita",
+ "will_enable": "Abilita messaggio di ultima volont\u00e0 e testamento",
"will_payload": "Payload del messaggio will",
"will_qos": "QoS del messaggio will",
"will_retain": "Persistenza del messaggio will",
diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json
index 7953c744f2713b..2b8119aca4d341 100644
--- a/homeassistant/components/mqtt/translations/nl.json
+++ b/homeassistant/components/mqtt/translations/nl.json
@@ -34,7 +34,17 @@
"button_4": "Vierde knop",
"button_5": "Vijfde knop",
"button_6": "Zesde knop",
- "turn_off": "Uitschakelen"
+ "turn_off": "Uitschakelen",
+ "turn_on": "Inschakelen"
+ }
+ },
+ "options": {
+ "step": {
+ "broker": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json
index b1863b90d1c9fd..4f958bab3e14c0 100644
--- a/homeassistant/components/mqtt/translations/no.json
+++ b/homeassistant/components/mqtt/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Kun en konfigurasjon av MQTT er tillatt."
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
- "cannot_connect": "Kan ikke koble til megleren."
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "Ugyldig f\u00f8dselsemne.",
"bad_will": "Ugyldig emne.",
- "cannot_connect": "Kan ikke koble til megleren."
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "F\u00f8dselsmelding behold",
"birth_topic": "F\u00f8dselsmelding emne",
"discovery": "Aktiver oppdagelse",
- "will_enable": "Aktiver f\u00f8dselsmelding",
+ "will_enable": "Aktiver will melding",
"will_payload": "Testament melding nyttelast",
"will_qos": "Testament melding QoS",
"will_retain": "Testament melding behold",
diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json
index 92c6d49e6773f3..13984b2bd5976d 100644
--- a/homeassistant/components/mqtt/translations/pl.json
+++ b/homeassistant/components/mqtt/translations/pl.json
@@ -66,11 +66,13 @@
},
"options": {
"data": {
+ "birth_enable": "W\u0142\u0105cz wiadomo\u015b\u0107 \"birth\"",
"birth_payload": "Warto\u015b\u0107 wiadomo\u015bci \"birth\"",
"birth_qos": "QoS wiadomo\u015bci \"birth\"",
"birth_retain": "Flaga \"retain\" wiadomo\u015bci \"birth\"",
"birth_topic": "Temat wiadomo\u015bci \"birth\"",
"discovery": "W\u0142\u0105cz wykrywanie",
+ "will_enable": "W\u0142\u0105cz wiadomo\u015b\u0107 \"will\"",
"will_payload": "Warto\u015b\u0107 wiadomo\u015bci \"will\"",
"will_qos": "QoS wiadomo\u015bci \"will\"",
"will_retain": "Flaga \"retain\" wiadomo\u015bci \"will\"",
diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json
index 4ff21126cfaad6..bfac4cadecc9ff 100644
--- a/homeassistant/components/mqtt/translations/ru.json
+++ b/homeassistant/components/mqtt/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.",
"bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.",
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443"
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
"birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)",
"discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435",
- "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
+ "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
"will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
"will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
"will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438",
diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json
index 02978a22327122..de92aee31717ed 100644
--- a/homeassistant/components/mqtt/translations/zh-Hant.json
+++ b/homeassistant/components/mqtt/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 MQTT\u3002"
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
- "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Broker\u3002"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
"broker": {
@@ -52,7 +52,7 @@
"error": {
"bad_birth": "Birth \u4e3b\u984c\u7121\u6548\u3002",
"bad_will": "Will \u4e3b\u984c\u7121\u6548\u3002",
- "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Broker\u3002"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
"broker": {
@@ -72,7 +72,7 @@
"birth_retain": "Birth \u8a0a\u606f Retain",
"birth_topic": "Birth \u8a0a\u606f\u4e3b\u984c",
"discovery": "\u958b\u555f\u63a2\u7d22",
- "will_enable": "\u958b\u555f Birth \u8a0a\u606f",
+ "will_enable": "\u958b\u555f Will \u8a0a\u606f",
"will_payload": "Will \u8a0a\u606f payload",
"will_qos": "Will \u8a0a\u606f QoS",
"will_retain": "Will \u8a0a\u606f Retain",
diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json
index 566b6a0fdacc7c..19717907b0ffa2 100644
--- a/homeassistant/components/myq/strings.json
+++ b/homeassistant/components/myq/strings.json
@@ -10,12 +10,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "MyQ is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/myq/translations/ca.json b/homeassistant/components/myq/translations/ca.json
index d22f3a39085e25..2b6549586a40be 100644
--- a/homeassistant/components/myq/translations/ca.json
+++ b/homeassistant/components/myq/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ ja est\u00e0 configurat"
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/myq/translations/en.json b/homeassistant/components/myq/translations/en.json
index 54392f3e2935de..9dad2d10cad481 100644
--- a/homeassistant/components/myq/translations/en.json
+++ b/homeassistant/components/myq/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ is already configured"
+ "already_configured": "Service is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/myq/translations/et.json b/homeassistant/components/myq/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/myq/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/translations/it.json b/homeassistant/components/myq/translations/it.json
index 9e0484dd6bb640..ac793e62c6d2b9 100644
--- a/homeassistant/components/myq/translations/it.json
+++ b/homeassistant/components/myq/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/myq/translations/no.json b/homeassistant/components/myq/translations/no.json
index 4db38e2b91fa89..1df4d0b60008f6 100644
--- a/homeassistant/components/myq/translations/no.json
+++ b/homeassistant/components/myq/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ er allerede konfigurert"
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json
index aefc8336903b36..d01f9e588fa8ce 100644
--- a/homeassistant/components/myq/translations/pl.json
+++ b/homeassistant/components/myq/translations/pl.json
@@ -5,8 +5,8 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json
index 81ec65f0d63864..daa3148beef481 100644
--- a/homeassistant/components/myq/translations/ru.json
+++ b/homeassistant/components/myq/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py
index 0bab6ea6eeae0f..4ec3c6e0abd65c 100644
--- a/homeassistant/components/mysensors/binary_sensor.py
+++ b/homeassistant/components/mysensors/binary_sensor.py
@@ -1,6 +1,11 @@
"""Support for MySensors binary sensors."""
from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_SAFETY,
+ DEVICE_CLASS_SOUND,
+ DEVICE_CLASS_VIBRATION,
DEVICE_CLASSES,
DOMAIN,
BinarySensorEntity,
@@ -9,13 +14,13 @@
SENSORS = {
"S_DOOR": "door",
- "S_MOTION": "motion",
+ "S_MOTION": DEVICE_CLASS_MOTION,
"S_SMOKE": "smoke",
- "S_SPRINKLER": "safety",
- "S_WATER_LEAK": "safety",
- "S_SOUND": "sound",
- "S_VIBRATION": "vibration",
- "S_MOISTURE": "moisture",
+ "S_SPRINKLER": DEVICE_CLASS_SAFETY,
+ "S_WATER_LEAK": DEVICE_CLASS_SAFETY,
+ "S_SOUND": DEVICE_CLASS_SOUND,
+ "S_VIBRATION": DEVICE_CLASS_VIBRATION,
+ "S_MOISTURE": DEVICE_CLASS_MOISTURE,
}
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index 8ff2139a7b42c4..6a6e95ddd0106b 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -9,12 +9,14 @@
ENERGY_KILO_WATT_HOUR,
FREQUENCY_HERTZ,
LENGTH_METERS,
+ LIGHT_LUX,
MASS_KILOGRAMS,
PERCENTAGE,
POWER_WATT,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
VOLT,
+ VOLUME_CUBIC_METERS,
)
SENSORS = {
@@ -36,11 +38,11 @@
"V_KWH": [ENERGY_KILO_WATT_HOUR, None],
"V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny"],
"V_FLOW": [LENGTH_METERS, "mdi:gauge"],
- "V_VOLUME": ["m³", None],
+ "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None],
"V_LEVEL": {
"S_SOUND": ["dB", "mdi:volume-high"],
"S_VIBRATION": [FREQUENCY_HERTZ, None],
- "S_LIGHT_LEVEL": ["lx", "mdi:white-balance-sunny"],
+ "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny"],
},
"V_VOLTAGE": [VOLT, "mdi:flash"],
"V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto"],
diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py
index 88a2085b33968f..f74364bc8bc38d 100644
--- a/homeassistant/components/neato/config_flow.py
+++ b/homeassistant/components/neato/config_flow.py
@@ -105,8 +105,8 @@ def try_login(username, password, vendor):
try:
Account(username, password, this_vendor)
except NeatoLoginException:
- return "invalid_credentials"
+ return "invalid_auth"
except NeatoRobotException:
- return "unexpected_error"
+ return "unknown"
return None
diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json
index 024a30a8e2d97a..5d71d4889acc04 100644
--- a/homeassistant/components/neato/strings.json
+++ b/homeassistant/components/neato/strings.json
@@ -12,15 +12,15 @@
}
},
"error": {
- "invalid_credentials": "Invalid credentials",
- "unexpected_error": "Unexpected error"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"create_entry": {
"default": "See [Neato documentation]({docs_url})."
},
"abort": {
- "already_configured": "Already configured",
- "invalid_credentials": "Invalid credentials"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json
index d1cfe9f7a911a4..c3d3426eb99a30 100644
--- a/homeassistant/components/neato/translations/ca.json
+++ b/homeassistant/components/neato/translations/ca.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Ja configurat",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Credencials inv\u00e0lides"
},
"create_entry": {
"default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})."
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Credencials inv\u00e0lides",
- "unexpected_error": "Error inesperat"
+ "unexpected_error": "Error inesperat",
+ "unknown": "Error inesperat"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/el.json b/homeassistant/components/neato/translations/el.json
new file mode 100644
index 00000000000000..36bd6653da6d43
--- /dev/null
+++ b/homeassistant/components/neato/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7"
+ },
+ "error": {
+ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7",
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json
index d41faddcc665ef..5cc832f39b759b 100644
--- a/homeassistant/components/neato/translations/en.json
+++ b/homeassistant/components/neato/translations/en.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Already configured",
+ "already_configured": "Device is already configured",
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid credentials"
},
"create_entry": {
"default": "See [Neato documentation]({docs_url})."
},
"error": {
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid credentials",
- "unexpected_error": "Unexpected error"
+ "unexpected_error": "Unexpected error",
+ "unknown": "Unexpected error"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json
index 766f05d31bf76d..13dbe19844c0ee 100644
--- a/homeassistant/components/neato/translations/es.json
+++ b/homeassistant/components/neato/translations/es.json
@@ -2,14 +2,17 @@
"config": {
"abort": {
"already_configured": "Ya est\u00e1 configurado",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Credenciales no v\u00e1lidas"
},
"create_entry": {
"default": "Ver [documentaci\u00f3n Neato]({docs_url})."
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Credenciales no v\u00e1lidas",
- "unexpected_error": "Error inesperado"
+ "unexpected_error": "Error inesperado",
+ "unknown": "Error inesperado"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json
new file mode 100644
index 00000000000000..4026f703b7ba9e
--- /dev/null
+++ b/homeassistant/components/neato/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "invalid_auth": "Tuvastamise viga"
+ },
+ "error": {
+ "invalid_auth": "Tuvastamise viga",
+ "unexpected_error": "Ootamatu t\u00f5rge",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi",
+ "vendor": "Tootja"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json
index 66f1bb52804300..11c06cd1c97b8b 100644
--- a/homeassistant/components/neato/translations/fr.json
+++ b/homeassistant/components/neato/translations/fr.json
@@ -2,14 +2,17 @@
"config": {
"abort": {
"already_configured": "D\u00e9j\u00e0 configur\u00e9",
+ "invalid_auth": "Authentification invalide",
"invalid_credentials": "Informations d'identification invalides"
},
"create_entry": {
"default": "Voir [Documentation Neato]({docs_url})."
},
"error": {
+ "invalid_auth": "Authentification invalide",
"invalid_credentials": "Informations d'identification invalides",
- "unexpected_error": "Erreur inattendue"
+ "unexpected_error": "Erreur inattendue",
+ "unknown": "Erreur inattendue"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json
index a456a2688e0225..654862f02594a8 100644
--- a/homeassistant/components/neato/translations/it.json
+++ b/homeassistant/components/neato/translations/it.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Gi\u00e0 configurato",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Credenziali non valide"
},
"create_entry": {
"default": "Vedere la [Documentazione di Neato]({docs_url})."
},
"error": {
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Credenziali non valide",
- "unexpected_error": "Errore inaspettato"
+ "unexpected_error": "Errore inaspettato",
+ "unknown": "Errore imprevisto"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json
index de8932cd570c93..57116666454ab3 100644
--- a/homeassistant/components/neato/translations/no.json
+++ b/homeassistant/components/neato/translations/no.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Allerede konfigurert",
+ "already_configured": "Enheten er allerede konfigurert",
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_credentials": "Ugyldig legitimasjon"
},
"create_entry": {
"default": "Se [Neato dokumentasjon]({docs_url})."
},
"error": {
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_credentials": "Ugyldig legitimasjon",
- "unexpected_error": "Uventet feil"
+ "unexpected_error": "Uventet feil",
+ "unknown": "Uventet feil"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json
index 80e0a1df48ef11..821cf79c971596 100644
--- a/homeassistant/components/neato/translations/pl.json
+++ b/homeassistant/components/neato/translations/pl.json
@@ -9,7 +9,7 @@
},
"error": {
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
- "unexpected_error": "Nieoczekiwany b\u0142\u0105d."
+ "unexpected_error": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json
index 92935906a865fe..cb899e4614a925 100644
--- a/homeassistant/components/neato/translations/ru.json
+++ b/homeassistant/components/neato/translations/ru.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"create_entry": {
"default": "\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]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
- "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json
index 8d8fe629030216..84d31b35e6d97e 100644
--- a/homeassistant/components/neato/translations/zh-Hant.json
+++ b/homeassistant/components/neato/translations/zh-Hant.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_credentials": "\u6191\u8b49\u7121\u6548"
},
"create_entry": {
"default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002"
},
"error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"invalid_credentials": "\u6191\u8b49\u7121\u6548",
- "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4"
+ "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py
index dd52e1d665fc2b..0e9198a0220d04 100644
--- a/homeassistant/components/nest/binary_sensor.py
+++ b/homeassistant/components/nest/binary_sensor.py
@@ -2,14 +2,20 @@
from itertools import chain
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OCCUPANCY,
+ DEVICE_CLASS_SOUND,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice
_LOGGER = logging.getLogger(__name__)
-BINARY_TYPES = {"online": "connectivity"}
+BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY}
CLIMATE_BINARY_TYPES = {
"fan": None,
@@ -19,9 +25,9 @@
}
CAMERA_BINARY_TYPES = {
- "motion_detected": "motion",
- "sound_detected": "sound",
- "person_detected": "occupancy",
+ "motion_detected": DEVICE_CLASS_MOTION,
+ "sound_detected": DEVICE_CLASS_SOUND,
+ "person_detected": DEVICE_CLASS_OCCUPANCY,
}
STRUCTURE_BINARY_TYPES = {"away": None}
@@ -153,7 +159,7 @@ def unique_id(self):
@property
def device_class(self):
"""Return the device class of the binary sensor."""
- return "motion"
+ return DEVICE_CLASS_MOTION
def update(self):
"""Retrieve latest state."""
diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py
index 8b0af5011ec02b..df03c05439862b 100644
--- a/homeassistant/components/nest/local_auth.py
+++ b/homeassistant/components/nest/local_auth.py
@@ -4,6 +4,7 @@
from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth
+from homeassistant.const import HTTP_UNAUTHORIZED
from homeassistant.core import callback
from . import config_flow
@@ -42,7 +43,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code):
await hass.async_add_job(auth.login)
return await result
except AuthorizationError as err:
- if err.response.status_code == 401:
+ if err.response.status_code == HTTP_UNAUTHORIZED:
raise config_flow.CodeInvalid()
raise config_flow.NestAuthError(
f"Unknown error: {err} ({err.response.status_code})"
diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py
index 3f9720f3adb2b1..dff6013c7c6989 100644
--- a/homeassistant/components/netatmo/camera.py
+++ b/homeassistant/components/netatmo/camera.py
@@ -284,9 +284,9 @@ def async_update_callback(self):
self._data.events.get(self._id, {})
)
elif self._model == "NOC": # Smart Outdoor Camera
- self.hass.data[DOMAIN][DATA_EVENTS][
- self._id
- ] = self._data.outdoor_events.get(self._id, {})
+ self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
+ self._data.outdoor_events.get(self._id, {})
+ )
def process_events(self, events):
"""Add meta data to events."""
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index f24591fe95467d..30ce38753c6d5e 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -315,7 +315,7 @@ def hvac_modes(self):
@property
def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation if supported."""
- if self._model == NA_THERM:
+ if self._model == NA_THERM and self._boilerstatus is not None:
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
# Maybe it is a valve
if self._room_status and self._room_status.get("heating_power_request", 0) > 0:
diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py
index 765276772242f7..6375c46d394f51 100644
--- a/homeassistant/components/netatmo/media_source.py
+++ b/homeassistant/components/netatmo/media_source.py
@@ -80,8 +80,20 @@ def _build_item_response(
) -> BrowseMediaSource:
if event_id and event_id in self.events[camera_id]:
created = dt.datetime.fromtimestamp(event_id)
- thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
- message = remove_html_tags(self.events[camera_id][event_id]["message"])
+ if self.events[camera_id][event_id]["type"] == "outdoor":
+ thumbnail = (
+ self.events[camera_id][event_id]["event_list"][0]
+ .get("snapshot", {})
+ .get("url")
+ )
+ message = remove_html_tags(
+ self.events[camera_id][event_id]["event_list"][0]["message"]
+ )
+ else:
+ thumbnail = (
+ self.events[camera_id][event_id].get("snapshot", {}).get("url")
+ )
+ message = remove_html_tags(self.events[camera_id][event_id]["message"])
title = f"{created} - {message}"
else:
title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index 2368d54efdf560..dc61f3d68cf5cc 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -12,6 +12,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_MBAR,
SPEED_KILOMETERS_PER_HOUR,
@@ -48,13 +49,15 @@
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
+ "temp_trend": ["Temperature trend", None, "mdi:trending-up", None],
"co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None],
"pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE],
+ "pressure_trend": ["Pressure trend", None, "mdi:trending-up", None],
"noise": ["Noise", "dB", "mdi:volume-high", None],
"humidity": ["Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY],
- "rain": ["Rain", "mm", "mdi:weather-rainy", None],
- "sum_rain_1": ["Rain last hour", "mm", "mdi:weather-rainy", None],
- "sum_rain_24": ["Rain last 24h", "mm", "mdi:weather-rainy", None],
+ "rain": ["Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None],
+ "sum_rain_1": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-rainy", None],
+ "sum_rain_24": ["Rain today", LENGTH_MILLIMETERS, "mdi:weather-rainy", None],
"battery_vp": ["Battery", "", "mdi:battery", None],
"battery_lvl": ["Battery Level", "", "mdi:battery", None],
"battery_percent": ["Battery Percent", PERCENTAGE, None, DEVICE_CLASS_BATTERY],
@@ -311,6 +314,8 @@ def async_update_callback(self):
try:
if self.type == "temperature":
self._state = round(data["Temperature"], 1)
+ elif self.type == "temp_trend":
+ self._state = data["temp_trend"]
elif self.type == "humidity":
self._state = data["Humidity"]
elif self.type == "rain":
@@ -325,6 +330,8 @@ def async_update_callback(self):
self._state = data["CO2"]
elif self.type == "pressure":
self._state = round(data["Pressure"], 1)
+ elif self.type == "pressure_trend":
+ self._state = data["pressure_trend"]
elif self.type == "battery_percent":
self._state = data["battery_percent"]
elif self.type == "battery_lvl":
@@ -336,15 +343,15 @@ def async_update_callback(self):
elif self.type == "max_temp":
self._state = data["max_temp"]
elif self.type == "windangle_value":
- self._state = data["WindAngle"]
+ self._state = fix_angle(data["WindAngle"])
elif self.type == "windangle":
- self._state = process_angle(data["WindAngle"])
+ self._state = process_angle(fix_angle(data["WindAngle"]))
elif self.type == "windstrength":
self._state = data["WindStrength"]
elif self.type == "gustangle_value":
- self._state = data["GustAngle"]
+ self._state = fix_angle(data["GustAngle"])
elif self.type == "gustangle":
- self._state = process_angle(data["GustAngle"])
+ self._state = process_angle(fix_angle(data["GustAngle"]))
elif self.type == "guststrength":
self._state = data["GustStrength"]
elif self.type == "reachable":
@@ -366,6 +373,13 @@ def async_update_callback(self):
return
+def fix_angle(angle: int) -> int:
+ """Fix angle when value is negative."""
+ if angle < 0:
+ return 360 + angle
+ return angle
+
+
def process_angle(angle: int) -> str:
"""Process angle and return string for display."""
if angle >= 330:
diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json
index a72728e8438660..1ad29ab080ddd4 100644
--- a/homeassistant/components/netatmo/translations/es.json
+++ b/homeassistant/components/netatmo/translations/es.json
@@ -3,6 +3,7 @@
"abort": {
"authorize_url_timeout": "Tiempo excedido generando la url de autorizaci\u00f3n.",
"missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})",
"single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
diff --git a/homeassistant/components/netatmo/translations/et.json b/homeassistant/components/netatmo/translations/et.json
new file mode 100644
index 00000000000000..cf0944dbe0e1c6
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/et.json
@@ -0,0 +1,27 @@
+{
+ "options": {
+ "step": {
+ "public_weather": {
+ "data": {
+ "area_name": "Ala nimi",
+ "lat_ne": "Kirdenurga laiuskraad",
+ "lat_sw": "Edelanurga laiuskraad",
+ "lon_ne": "Kirdenurga pikkuskraad",
+ "lon_sw": "Edelanurga pikkuskraad",
+ "mode": "Arvutamine",
+ "show_on_map": "Kuva kaardil"
+ },
+ "description": "Seadista selle ala avalik ilmaandur.",
+ "title": "Netatmo avalik ilmaandur"
+ },
+ "public_weather_areas": {
+ "data": {
+ "new_area": "Ala nimi",
+ "weather_areas": "Ilmaandmete alad"
+ },
+ "description": "Seadista avalikke ilmastikuandureid.",
+ "title": "Netatmo avalik ilmaandur"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json
index fd4072dc54ab46..fe8fc74d273473 100644
--- a/homeassistant/components/netatmo/translations/fr.json
+++ b/homeassistant/components/netatmo/translations/fr.json
@@ -3,6 +3,7 @@
"abort": {
"authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
"missing_configuration": "Ce 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} )",
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json
index f8c052bd5f8aff..8165941f0d8fa1 100644
--- a/homeassistant/components/netatmo/translations/ko.json
+++ b/homeassistant/components/netatmo/translations/ko.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"create_entry": {
"default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/netatmo/translations/lb.json b/homeassistant/components/netatmo/translations/lb.json
index f46b8627e0c16f..6f71b98fd936cd 100644
--- a/homeassistant/components/netatmo/translations/lb.json
+++ b/homeassistant/components/netatmo/translations/lb.json
@@ -3,6 +3,7 @@
"abort": {
"authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL",
"missing_configuration": "D\u00ebs Komponent ass net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})",
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
},
"create_entry": {
@@ -28,7 +29,8 @@
"data": {
"new_area": "Numm vum Ber\u00e4ich",
"weather_areas": "Wieder Ber\u00e4icher"
- }
+ },
+ "title": "Netatmo public weather sensor"
}
}
}
diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json
index 5cc1d400719afc..98e3206f46a830 100644
--- a/homeassistant/components/netatmo/translations/no.json
+++ b/homeassistant/components/netatmo/translations/no.json
@@ -3,6 +3,7 @@
"abort": {
"authorize_url_timeout": "Tidsavbrutt ved oppretting av godkjennings url.",
"missing_configuration": "Komponeneten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json
index 8a549e4cd3070b..721c3a2a946a2b 100644
--- a/homeassistant/components/netatmo/translations/pl.json
+++ b/homeassistant/components/netatmo/translations/pl.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
- "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
+ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono"
diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json
index fd528ed4cfcf44..588675c670e734 100644
--- a/homeassistant/components/netatmo/translations/zh-Hant.json
+++ b/homeassistant/components/netatmo/translations/zh-Hant.json
@@ -3,6 +3,7 @@
"abort": {
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py
index e354c84e715dfc..644fe35c8c3eb3 100644
--- a/homeassistant/components/netgear_lte/sensor_types.py
+++ b/homeassistant/components/netgear_lte/sensor_types.py
@@ -1,7 +1,11 @@
"""Define possible sensor types."""
from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY
-from homeassistant.const import DATA_MEBIBYTES, PERCENTAGE
+from homeassistant.const import (
+ DATA_MEBIBYTES,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+)
SENSOR_SMS = "sms"
SENSOR_SMS_TOTAL = "sms_total"
@@ -12,8 +16,8 @@
SENSOR_SMS_TOTAL: "messages",
SENSOR_USAGE: DATA_MEBIBYTES,
"radio_quality": PERCENTAGE,
- "rx_level": "dBm",
- "tx_level": "dBm",
+ "rx_level": SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ "tx_level": SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
"upstream": None,
"connection_text": None,
"connection_type": None,
diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json
index dcfb40b898ac65..876ea2d656f11b 100644
--- a/homeassistant/components/nexia/strings.json
+++ b/homeassistant/components/nexia/strings.json
@@ -10,12 +10,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "This nexia home is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/nexia/translations/ca.json b/homeassistant/components/nexia/translations/ca.json
index fcbb879548cb4f..beb5b1a1d9dcfe 100644
--- a/homeassistant/components/nexia/translations/ca.json
+++ b/homeassistant/components/nexia/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu nexia home ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/nexia/translations/en.json b/homeassistant/components/nexia/translations/en.json
index 016518495dcdde..fad0b8e542a568 100644
--- a/homeassistant/components/nexia/translations/en.json
+++ b/homeassistant/components/nexia/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "This nexia home is already configured"
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/nexia/translations/et.json b/homeassistant/components/nexia/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/nexia/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nexia/translations/it.json b/homeassistant/components/nexia/translations/it.json
index 034aff83ecdab1..254617d718be90 100644
--- a/homeassistant/components/nexia/translations/it.json
+++ b/homeassistant/components/nexia/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo Nexia Home \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/nexia/translations/no.json b/homeassistant/components/nexia/translations/no.json
index 335d8834ec388a..dc4ed03f46e451 100644
--- a/homeassistant/components/nexia/translations/no.json
+++ b/homeassistant/components/nexia/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dette nexia hjem er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json
index 84844ddbd9483b..7f09569418d373 100644
--- a/homeassistant/components/nexia/translations/pl.json
+++ b/homeassistant/components/nexia/translations/pl.json
@@ -5,8 +5,8 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json
index 1dd3be26fd13f7..a19d16e3a7e696 100644
--- a/homeassistant/components/nexia/translations/ru.json
+++ b/homeassistant/components/nexia/translations/ru.json
@@ -1,10 +1,10 @@
{
"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\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py
index 12c17e6081d5bf..1a773040980781 100644
--- a/homeassistant/components/nextcloud/__init__.py
+++ b/homeassistant/components/nextcloud/__init__.py
@@ -111,6 +111,7 @@ def nextcloud_update(event_time):
return False
hass.data[DOMAIN] = get_data_points(ncm.data)
+ hass.data[DOMAIN]["instance"] = conf[CONF_URL]
# Update sensors on time interval
track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL])
diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py
index 369a5c875accef..e71d81f2b79048 100644
--- a/homeassistant/components/nfandroidtv/notify.py
+++ b/homeassistant/components/nfandroidtv/notify.py
@@ -158,26 +158,26 @@ def send_message(self, message="", **kwargs):
"""Send a message to a Android TV device."""
_LOGGER.debug("Sending notification to: %s", self._target)
- payload = dict(
- filename=(
+ payload = {
+ "filename": (
"icon.png",
self._icon_file,
"application/octet-stream",
{"Expires": "0"},
),
- type="0",
- title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
- msg=message,
- duration="%i" % self._default_duration,
- fontsize="%i" % FONTSIZES.get(self._default_fontsize),
- position="%i" % POSITIONS.get(self._default_position),
- bkgcolor="%s" % COLORS.get(self._default_color),
- transparency="%i" % TRANSPARENCIES.get(self._default_transparency),
- offset="0",
- app=ATTR_TITLE_DEFAULT,
- force="true",
- interrupt="%i" % self._default_interrupt,
- )
+ "type": "0",
+ "title": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
+ "msg": message,
+ "duration": "%i" % self._default_duration,
+ "fontsize": "%i" % FONTSIZES.get(self._default_fontsize),
+ "position": "%i" % POSITIONS.get(self._default_position),
+ "bkgcolor": "%s" % COLORS.get(self._default_color),
+ "transparency": "%i" % TRANSPARENCIES.get(self._default_transparency),
+ "offset": "0",
+ "app": ATTR_TITLE_DEFAULT,
+ "force": "true",
+ "interrupt": "%i" % self._default_interrupt,
+ }
data = kwargs.get(ATTR_DATA)
if data:
diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py
index 88939cbe7906a0..1b67963bcc3985 100644
--- a/homeassistant/components/nightscout/__init__.py
+++ b/homeassistant/components/nightscout/__init__.py
@@ -7,7 +7,7 @@
from py_nightscout import Api as NightscoutAPI
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_URL
+from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
@@ -30,8 +30,9 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Nightscout from a config entry."""
server_url = entry.data[CONF_URL]
+ api_key = entry.data.get(CONF_API_KEY)
session = async_get_clientsession(hass)
- api = NightscoutAPI(server_url, session=session)
+ api = NightscoutAPI(server_url, session=session, api_secret=api_key)
try:
status = await api.get_server_status()
except (ClientError, AsyncIOTimeoutError, OSError) as error:
diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py
index bd33bc8dcb4d24..3000d652e46f87 100644
--- a/homeassistant/components/nightscout/config_flow.py
+++ b/homeassistant/components/nightscout/config_flow.py
@@ -2,27 +2,32 @@
from asyncio import TimeoutError as AsyncIOTimeoutError
import logging
-from aiohttp import ClientError
+from aiohttp import ClientError, ClientResponseError
from py_nightscout import Api as NightscoutAPI
import voluptuous as vol
from homeassistant import config_entries, exceptions
-from homeassistant.const import CONF_URL
+from homeassistant.const import CONF_API_KEY, CONF_URL
from .const import DOMAIN # pylint:disable=unused-import
from .utils import hash_from_url
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str})
+DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str, vol.Optional(CONF_API_KEY): str})
async def _validate_input(data):
"""Validate the user input allows us to connect."""
url = data[CONF_URL]
+ api_key = data.get(CONF_API_KEY)
try:
- api = NightscoutAPI(url)
+ api = NightscoutAPI(url, api_secret=api_key)
status = await api.get_server_status()
+ if status.settings.get("authDefaultRoles") == "status-only":
+ await api.get_sgvs()
+ except ClientResponseError as error:
+ raise InputValidationError("invalid_auth") from error
except (ClientError, AsyncIOTimeoutError, OSError) as error:
raise InputValidationError("cannot_connect") from error
diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json
index b3e9b3a0d555d4..ecc44258e907ff 100644
--- a/homeassistant/components/nightscout/manifest.json
+++ b/homeassistant/components/nightscout/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nightscout",
"requirements": [
- "py-nightscout==1.2.1"
+ "py-nightscout==1.2.2"
],
"codeowners": [
"@marciogranzotto"
diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json
index a6e100ae8f29fc..2240bcec02b571 100644
--- a/homeassistant/components/nightscout/strings.json
+++ b/homeassistant/components/nightscout/strings.json
@@ -3,12 +3,16 @@
"flow_title": "Nightscout",
"step": {
"user": {
+ "title": "Enter your Nightscout server information.",
+ "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).",
"data": {
- "url": "URL"
+ "url": "[%key:common::config_flow::data::url%]",
+ "api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
"error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
@@ -16,4 +20,4 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json
index eb06d94de3b396..21a472680b4aa9 100644
--- a/homeassistant/components/nightscout/translations/ca.json
+++ b/homeassistant/components/nightscout/translations/ca.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "Clau API",
"url": "URL"
- }
+ },
+ "description": "- URL: l'adre\u00e7a de la teva inst\u00e0ncia de Nightscout. Per exemple: https://myhomeassistant.duckdns.org:5423 \n- Clau API (opcional): utilitza-la nom\u00e9s si la teva inst\u00e0ncia est\u00e0 protegida (auth_default_roles != readable).",
+ "title": "Introdueix la informaci\u00f3 del teu servidor Nightscout."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json
new file mode 100644
index 00000000000000..a7ad0fe1d27c43
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json
new file mode 100644
index 00000000000000..42762bfddce8c9
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/el.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API"
+ },
+ "description": "- \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 nightcout. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n - \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03b5\u03c4\u03b1\u03b9 (auth_default_roles! = readable).",
+ "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Nightscout."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json
index b7947c84997a63..d8b4c441283ec7 100644
--- a/homeassistant/components/nightscout/translations/en.json
+++ b/homeassistant/components/nightscout/translations/en.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API Key",
"url": "URL"
- }
+ },
+ "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).",
+ "title": "Enter your Nightscout server information."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json
index 9a03055bac4495..307b8ede5aab57 100644
--- a/homeassistant/components/nightscout/translations/es.json
+++ b/homeassistant/components/nightscout/translations/es.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "Clave API",
"url": "URL"
- }
+ },
+ "description": "- URL: la direcci\u00f3n de tu instancia de nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423 \n - Clave API (opcional): util\u00edzala s\u00f3lo si tu instancia est\u00e1 protegida (auth_default_roles! = readable).",
+ "title": "Introduce la informaci\u00f3n del servidor de Nightscout."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/et.json b/homeassistant/components/nightscout/translations/et.json
new file mode 100644
index 00000000000000..cc02e4ab9642ca
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/et.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json
index 9ed1ea1bfd1004..1bcd530d29b52f 100644
--- a/homeassistant/components/nightscout/translations/fr.json
+++ b/homeassistant/components/nightscout/translations/fr.json
@@ -5,11 +5,14 @@
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
"unknown": "Erreur inattendue"
},
+ "flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "Cl\u00e9 d'API",
"url": "URL"
}
}
diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/it.json b/homeassistant/components/nightscout/translations/it.json
index e0305d1b5e2546..8a1bfc826e2301 100644
--- a/homeassistant/components/nightscout/translations/it.json
+++ b/homeassistant/components/nightscout/translations/it.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "Chiave API",
"url": "URL"
- }
+ },
+ "description": "- URL: l'indirizzo della tua istanza nightscout. Ad es.: https://myhomeassistant.duckdns.org:5423\n- Chiave API (opzionale): utilizzare solo se l'istanza \u00e8 protetta (auth_default_roles != readable).",
+ "title": "Inserisci le informazioni del tuo server Nightscout."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json
index 17dee71d640a46..0235c446e75454 100644
--- a/homeassistant/components/nightscout/translations/ko.json
+++ b/homeassistant/components/nightscout/translations/ko.json
@@ -2,6 +2,9 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/lb.json b/homeassistant/components/nightscout/translations/lb.json
index 76f6a7f233c5b5..cc1f048e84e95e 100644
--- a/homeassistant/components/nightscout/translations/lb.json
+++ b/homeassistant/components/nightscout/translations/lb.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
"unknown": "Onerwaarte Feeler"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API Schl\u00ebssel",
"url": "URL"
- }
+ },
+ "description": "- URL. Adresse vun denger Nightscout Instanz: beispill: \nhttps://myhomeassistant.duckdns.org:5423\n- API Schl\u00ebssel (optionell): N\u00ebmmen benotzen wann deng Instanz proteg\u00e9iert ass. \n(auth_default_roles != readable)",
+ "title": "F\u00ebll d\u00e9ng Nightscout Server Informatiounen aus."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json
index a586083f5694ed..7f600268e88845 100644
--- a/homeassistant/components/nightscout/translations/no.json
+++ b/homeassistant/components/nightscout/translations/no.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API-n\u00f8kkel",
"url": "URL"
- }
+ },
+ "description": "- URL: adressen til din nattscout-forekomst. Dvs: https://myhomeassistant.duckdns.org:5423 \n - API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = Lesbar).",
+ "title": "Skriv inn informasjon om Nightscout-serveren."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json
new file mode 100644
index 00000000000000..2883286c78fd97
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API"
+ },
+ "description": "- URL: adres url Twojej instancji nightcout. Np: https://myhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).",
+ "title": "Wprowad\u017a informacje o serwerze Nightscout."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json
index cf904f0134c6dd..738c4dfa9a33d0 100644
--- a/homeassistant/components/nightscout/translations/ru.json
+++ b/homeassistant/components/nightscout/translations/ru.json
@@ -5,14 +5,18 @@
},
"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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
"url": "URL-\u0430\u0434\u0440\u0435\u0441"
- }
+ },
+ "description": "- URL: \u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Nightscout. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: https://myhomeassistant.duckdns.org:5423\n- \u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e): \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 Nightcout \u0437\u0430\u0449\u0438\u0449\u0435\u043d (auth_default_roles != readable).",
+ "title": "Nightscout"
}
}
}
diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json
index fa9f3d12427a31..5066f5a2edb7eb 100644
--- a/homeassistant/components/nightscout/translations/zh-Hant.json
+++ b/homeassistant/components/nightscout/translations/zh-Hant.json
@@ -5,14 +5,18 @@
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API \u5bc6\u9470",
"url": "\u7db2\u5740"
- }
+ },
+ "description": "- URL\uff1aNightscout \u8a2d\u5099\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u8a2d\u5099\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002",
+ "title": "\u8f38\u5165 Nightscout \u4f3a\u670d\u5668\u8cc7\u8a0a\u3002"
}
}
}
diff --git a/homeassistant/components/notify/translations/et.json b/homeassistant/components/notify/translations/et.json
index d2c08643c0683c..798972fe384345 100644
--- a/homeassistant/components/notify/translations/et.json
+++ b/homeassistant/components/notify/translations/et.json
@@ -1,3 +1,3 @@
{
- "title": "Teata"
+ "title": "Teavitused"
}
\ No newline at end of file
diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py
index e798b53856585f..3702688eb94fdf 100644
--- a/homeassistant/components/notion/binary_sensor.py
+++ b/homeassistant/components/notion/binary_sensor.py
@@ -2,7 +2,14 @@
import logging
from typing import Callable
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_SMOKE,
+ DEVICE_CLASS_WINDOW,
+ BinarySensorEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@@ -26,15 +33,15 @@
BINARY_SENSOR_TYPES = {
SENSOR_BATTERY: ("Low Battery", "battery"),
- SENSOR_DOOR: ("Door", "door"),
+ SENSOR_DOOR: ("Door", DEVICE_CLASS_DOOR),
SENSOR_GARAGE_DOOR: ("Garage Door", "garage_door"),
- SENSOR_LEAK: ("Leak Detector", "moisture"),
- SENSOR_MISSING: ("Missing", "connectivity"),
- SENSOR_SAFE: ("Safe", "door"),
- SENSOR_SLIDING: ("Sliding Door/Window", "door"),
- SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", "smoke"),
- SENSOR_WINDOW_HINGED_HORIZONTAL: ("Hinged Window", "window"),
- SENSOR_WINDOW_HINGED_VERTICAL: ("Hinged Window", "window"),
+ SENSOR_LEAK: ("Leak Detector", DEVICE_CLASS_MOISTURE),
+ SENSOR_MISSING: ("Missing", DEVICE_CLASS_CONNECTIVITY),
+ SENSOR_SAFE: ("Safe", DEVICE_CLASS_DOOR),
+ SENSOR_SLIDING: ("Sliding Door/Window", DEVICE_CLASS_DOOR),
+ SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", DEVICE_CLASS_SMOKE),
+ SENSOR_WINDOW_HINGED_HORIZONTAL: ("Hinged Window", DEVICE_CLASS_WINDOW),
+ SENSOR_WINDOW_HINGED_VERTICAL: ("Hinged Window", DEVICE_CLASS_WINDOW),
}
diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py
index 58c5c0d44eea62..bcbd22bccc6bde 100644
--- a/homeassistant/components/notion/config_flow.py
+++ b/homeassistant/components/notion/config_flow.py
@@ -47,6 +47,6 @@ async def async_step_user(self, user_input=None):
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session
)
except NotionError:
- return await self._show_form({"base": "invalid_credentials"})
+ return await self._show_form({"base": "invalid_auth"})
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
diff --git a/homeassistant/components/notion/strings.json b/homeassistant/components/notion/strings.json
index 1fbe4837f7e7d7..401f0095e30837 100644
--- a/homeassistant/components/notion/strings.json
+++ b/homeassistant/components/notion/strings.json
@@ -10,11 +10,11 @@
}
},
"error": {
- "invalid_credentials": "Invalid username or password",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_devices": "No devices found in account"
},
"abort": {
- "already_configured": "This username is already in use."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/notion/translations/ca.json b/homeassistant/components/notion/translations/ca.json
index 05626cbd483cb2..7abc95e5990edc 100644
--- a/homeassistant/components/notion/translations/ca.json
+++ b/homeassistant/components/notion/translations/ca.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquest nom d'usuari ja est\u00e0 en \u00fas."
+ "already_configured": "El compte ja ha estat configurat"
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Nom d'usuari o contrasenya incorrectes",
"no_devices": "No s'han trobat dispositius al compte"
},
diff --git a/homeassistant/components/notion/translations/en.json b/homeassistant/components/notion/translations/en.json
index d70aa73824abd4..b092cc000d9d6d 100644
--- a/homeassistant/components/notion/translations/en.json
+++ b/homeassistant/components/notion/translations/en.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "This username is already in use."
+ "already_configured": "Account is already configured"
},
"error": {
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid username or password",
"no_devices": "No devices found in account"
},
diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json
index 2ea7fbb0db79c4..40f2f1af14fd96 100644
--- a/homeassistant/components/notion/translations/es.json
+++ b/homeassistant/components/notion/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "Este nombre de usuario ya est\u00e1 en uso."
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Usuario o contrase\u00f1a no v\u00e1lido",
"no_devices": "No se han encontrado dispositivos en la cuenta"
},
diff --git a/homeassistant/components/notion/translations/et.json b/homeassistant/components/notion/translations/et.json
new file mode 100644
index 00000000000000..2227b7442a79c6
--- /dev/null
+++ b/homeassistant/components/notion/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/translations/it.json b/homeassistant/components/notion/translations/it.json
index c7626f592029e5..732982278680c1 100644
--- a/homeassistant/components/notion/translations/it.json
+++ b/homeassistant/components/notion/translations/it.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo nome utente \u00e8 gi\u00e0 in uso."
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Nome utente o password non validi",
"no_devices": "Nessun dispositivo trovato nell'account"
},
diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json
index 907ee235c965ce..0ac19623c1aa64 100644
--- a/homeassistant/components/notion/translations/ru.json
+++ b/homeassistant/components/notion/translations/ru.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
+ "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": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
"no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e."
},
diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py
index 5e9a9835bf494b..b6c0d1a5d9bd0c 100644
--- a/homeassistant/components/nsw_fuel_station/sensor.py
+++ b/homeassistant/components/nsw_fuel_station/sensor.py
@@ -7,7 +7,7 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -179,7 +179,7 @@ def device_state_attributes(self) -> dict:
@property
def unit_of_measurement(self) -> str:
"""Return the units of measurement."""
- return "¢/L"
+ return f"{CURRENCY_CENT}/{VOLUME_LITERS}"
def update(self):
"""Update current conditions."""
diff --git a/homeassistant/components/nuheat/strings.json b/homeassistant/components/nuheat/strings.json
index 367420178e630d..aebad40e6a998c 100644
--- a/homeassistant/components/nuheat/strings.json
+++ b/homeassistant/components/nuheat/strings.json
@@ -1,13 +1,13 @@
{
"config": {
"error": {
- "unknown": "Unexpected error",
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_thermostat": "The thermostat serial number is invalid."
},
"abort": {
- "already_configured": "The thermostat is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"step": {
"user": {
@@ -21,4 +21,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/nuheat/translations/ca.json b/homeassistant/components/nuheat/translations/ca.json
index c7821838f4f71f..cba3a76ad8fd0e 100644
--- a/homeassistant/components/nuheat/translations/ca.json
+++ b/homeassistant/components/nuheat/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "El term\u00f2stat ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_thermostat": "El n\u00famero de s\u00e8rie del term\u00f2stat no \u00e9s v\u00e0lid.",
"unknown": "Error inesperat"
diff --git a/homeassistant/components/nuheat/translations/en.json b/homeassistant/components/nuheat/translations/en.json
index d7b2697d5e057a..73d6caa94e3c23 100644
--- a/homeassistant/components/nuheat/translations/en.json
+++ b/homeassistant/components/nuheat/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "The thermostat is already configured"
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"invalid_thermostat": "The thermostat serial number is invalid.",
"unknown": "Unexpected error"
diff --git a/homeassistant/components/nuheat/translations/et.json b/homeassistant/components/nuheat/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuheat/translations/fr.json b/homeassistant/components/nuheat/translations/fr.json
index da5c3260f2a43d..f0e912805edf7d 100644
--- a/homeassistant/components/nuheat/translations/fr.json
+++ b/homeassistant/components/nuheat/translations/fr.json
@@ -16,6 +16,7 @@
"serial_number": "Num\u00e9ro de s\u00e9rie du thermostat.",
"username": "Nom d'utilisateur"
},
+ "description": "Vous devrez obtenir le num\u00e9ro de s\u00e9rie ou l'identifiant num\u00e9rique de votre thermostat en vous connectant \u00e0 https://MyNuHeat.com et en s\u00e9lectionnant votre (vos) thermostat (s).",
"title": "Connectez-vous au NuHeat"
}
}
diff --git a/homeassistant/components/nuheat/translations/it.json b/homeassistant/components/nuheat/translations/it.json
index 973ff695acbf19..1e17ce8ecf8cbd 100644
--- a/homeassistant/components/nuheat/translations/it.json
+++ b/homeassistant/components/nuheat/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Il termostato \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"invalid_thermostat": "Il numero di serie del termostato non \u00e8 valido.",
"unknown": "Errore imprevisto"
diff --git a/homeassistant/components/nuheat/translations/no.json b/homeassistant/components/nuheat/translations/no.json
index 7ec631197cc60a..199a3be91ce62f 100644
--- a/homeassistant/components/nuheat/translations/no.json
+++ b/homeassistant/components/nuheat/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Termostaten er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"invalid_thermostat": "Termostatens serienummer er ugyldig.",
"unknown": "Uventet feil"
diff --git a/homeassistant/components/nuheat/translations/pl.json b/homeassistant/components/nuheat/translations/pl.json
index d55b545d040dda..d992afe9cc0cb3 100644
--- a/homeassistant/components/nuheat/translations/pl.json
+++ b/homeassistant/components/nuheat/translations/pl.json
@@ -5,9 +5,9 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"invalid_thermostat": "Numer seryjny termostatu jest nieprawid\u0142owy.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/nuheat/translations/ru.json b/homeassistant/components/nuheat/translations/ru.json
index f4d06e8eca64ab..09e74c0e4cbf8a 100644
--- a/homeassistant/components/nuheat/translations/ru.json
+++ b/homeassistant/components/nuheat/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py
index 8efc56e12fef7c..1730ff829566a1 100644
--- a/homeassistant/components/numato/__init__.py
+++ b/homeassistant/components/numato/__init__.py
@@ -12,6 +12,7 @@
CONF_SWITCHES,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
+ PERCENTAGE,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
@@ -31,7 +32,6 @@
DEFAULT_INVERT_LOGIC = False
DEFAULT_SRC_RANGE = [0, 1024]
DEFAULT_DST_RANGE = [0.0, 100.0]
-DEFAULT_UNIT = "%"
DEFAULT_DEV = [f"/dev/ttyACM{i}" for i in range(10)]
PORT_RANGE = range(1, 8) # ports 0-7 are ADC capable
@@ -82,7 +82,7 @@ def adc_port_number(num):
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_SRC_RANGE, default=DEFAULT_SRC_RANGE): int_range,
vol.Optional(CONF_DST_RANGE, default=DEFAULT_DST_RANGE): float_range,
- vol.Optional(CONF_DST_UNIT, default=DEFAULT_UNIT): cv.string,
+ vol.Optional(CONF_DST_UNIT, default=PERCENTAGE): cv.string,
}
)
@@ -171,7 +171,7 @@ class NumatoAPI:
def __init__(self):
"""Initialize API state."""
- self.ports_registered = dict()
+ self.ports_registered = {}
def check_port_free(self, device_id, port, direction):
"""Check whether a port is still free set up.
diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json
index 2203a501ff286e..1b71280b6a95c8 100644
--- a/homeassistant/components/nut/strings.json
+++ b/homeassistant/components/nut/strings.json
@@ -25,11 +25,11 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -43,4 +43,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/nut/translations/ca.json b/homeassistant/components/nut/translations/ca.json
index 13c1bde317363e..fa7b728e115272 100644
--- a/homeassistant/components/nut/translations/ca.json
+++ b/homeassistant/components/nut/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
"step": {
diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json
index f698ad9287aad6..2e5db79d81c328 100644
--- a/homeassistant/components/nut/translations/en.json
+++ b/homeassistant/components/nut/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json
index 177fd72067e102..440cb421504a47 100644
--- a/homeassistant/components/nut/translations/it.json
+++ b/homeassistant/components/nut/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
"step": {
diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json
index 6fd749442c3bdc..8b1704fe04ae95 100644
--- a/homeassistant/components/nut/translations/no.json
+++ b/homeassistant/components/nut/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
"step": {
diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json
index 5fb9082d6764ad..d24638341d27bb 100644
--- a/homeassistant/components/nut/translations/pl.json
+++ b/homeassistant/components/nut/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"resources": {
diff --git a/homeassistant/components/nut/translations/ru.json b/homeassistant/components/nut/translations/ru.json
index 012ac2ae568f8d..7a3f1c9b47e0fb 100644
--- a/homeassistant/components/nut/translations/ru.json
+++ b/homeassistant/components/nut/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json
index 35cd266854f07f..b75bd37958f276 100644
--- a/homeassistant/components/nut/translations/zh-Hant.json
+++ b/homeassistant/components/nut/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json
index 23643699f3a892..ef0a35b846a68d 100644
--- a/homeassistant/components/nws/manifest.json
+++ b/homeassistant/components/nws/manifest.json
@@ -3,7 +3,7 @@
"name": "National Weather Service (NWS)",
"documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"],
- "requirements": ["pynws==1.2.1"],
+ "requirements": ["pynws==1.3.0"],
"quality_scale": "platinum",
"config_flow": true
}
diff --git a/homeassistant/components/nws/strings.json b/homeassistant/components/nws/strings.json
index 83e8a3a694bcab..0f0bdcf4a1cb62 100644
--- a/homeassistant/components/nws/strings.json
+++ b/homeassistant/components/nws/strings.json
@@ -6,18 +6,18 @@
"title": "Connect to the National Weather Service",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
- "latitude": "Latitude",
- "longitude": "Longitude",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
"station": "METAR station code"
}
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/nws/translations/ca.json b/homeassistant/components/nws/translations/ca.json
index a2239f5f97dbd7..e012d68a10e2d1 100644
--- a/homeassistant/components/nws/translations/ca.json
+++ b/homeassistant/components/nws/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat"
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat"
},
"step": {
diff --git a/homeassistant/components/nws/translations/en.json b/homeassistant/components/nws/translations/en.json
index 19eec6a20e3920..04cb13bf5e80d3 100644
--- a/homeassistant/components/nws/translations/en.json
+++ b/homeassistant/components/nws/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Service is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
diff --git a/homeassistant/components/nws/translations/et.json b/homeassistant/components/nws/translations/et.json
new file mode 100644
index 00000000000000..a9607835b439f6
--- /dev/null
+++ b/homeassistant/components/nws/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ },
+ "title": "\u00dchendu riikliku ilmateenistusega (USA)"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/translations/fr.json b/homeassistant/components/nws/translations/fr.json
index 86db25ebd11ebf..568179cf9fae20 100644
--- a/homeassistant/components/nws/translations/fr.json
+++ b/homeassistant/components/nws/translations/fr.json
@@ -15,6 +15,7 @@
"longitude": "Longitude",
"station": "Code de la station METAR"
},
+ "description": "Si aucun code de station METAR n'est sp\u00e9cifi\u00e9, la latitude et la longitude seront utilis\u00e9es pour trouver la station la plus proche.",
"title": "Se connecter au National Weather Service"
}
}
diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json
index 92c519513b4ce8..827d8078b55aad 100644
--- a/homeassistant/components/nws/translations/it.json
+++ b/homeassistant/components/nws/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto"
},
"step": {
diff --git a/homeassistant/components/nws/translations/no.json b/homeassistant/components/nws/translations/no.json
index f26abdeaa2e443..ba7f4c7a26d031 100644
--- a/homeassistant/components/nws/translations/no.json
+++ b/homeassistant/components/nws/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert"
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil"
},
"step": {
diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json
index ab1011d9d56617..2671f0408c9499 100644
--- a/homeassistant/components/nws/translations/pl.json
+++ b/homeassistant/components/nws/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/nws/translations/ru.json b/homeassistant/components/nws/translations/ru.json
index 96ab63cd2d435a..926e936a594389 100644
--- a/homeassistant/components/nws/translations/ru.json
+++ b/homeassistant/components/nws/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/nws/translations/zh-Hant.json b/homeassistant/components/nws/translations/zh-Hant.json
index 703dbd240b8681..067234c7b54e13 100644
--- a/homeassistant/components/nws/translations/zh-Hant.json
+++ b/homeassistant/components/nws/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py
index 3c641447e84f97..69dac297b1b7cc 100644
--- a/homeassistant/components/nws/weather.py
+++ b/homeassistant/components/nws/weather.py
@@ -318,3 +318,8 @@ async def async_update(self):
"""
await self.coordinator_observation.async_request_refresh()
await self.coordinator_forecast.async_request_refresh()
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self.mode == DAYNIGHT
diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py
index 2df7cd1bb4b04c..6366831b598c16 100644
--- a/homeassistant/components/nx584/alarm_control_panel.py
+++ b/homeassistant/components/nx584/alarm_control_panel.py
@@ -58,7 +58,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
except requests.exceptions.ConnectionError as ex:
_LOGGER.error(
"Unable to connect to %(host)s: %(reason)s",
- dict(host=url, reason=ex),
+ {"host": url, "reason": ex},
)
raise PlatformNotReady from ex
@@ -118,7 +118,7 @@ def update(self):
except requests.exceptions.ConnectionError as ex:
_LOGGER.error(
"Unable to connect to %(host)s: %(reason)s",
- dict(host=self._url, reason=ex),
+ {"host": self._url, "reason": ex},
)
self._state = None
zones = []
@@ -132,7 +132,7 @@ def update(self):
if zone["bypassed"]:
_LOGGER.debug(
"Zone %(zone)s is bypassed, assuming HOME",
- dict(zone=zone["number"]),
+ {"zone": zone["number"]},
)
bypassed = True
break
diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py
index 127ce02b371700..2db3531f879555 100644
--- a/homeassistant/components/nx584/binary_sensor.py
+++ b/homeassistant/components/nx584/binary_sensor.py
@@ -8,6 +8,7 @@
import voluptuous as vol
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OPENING,
DEVICE_CLASSES,
PLATFORM_SCHEMA,
BinarySensorEntity,
@@ -59,7 +60,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return False
zone_sensors = {
- zone["number"]: NX584ZoneSensor(zone, zone_types.get(zone["number"], "opening"))
+ zone["number"]: NX584ZoneSensor(
+ zone, zone_types.get(zone["number"], DEVICE_CLASS_OPENING)
+ )
for zone in zones
if zone["number"] not in exclude
}
diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py
index 130fa0d55b8528..467cd8c06dbc18 100644
--- a/homeassistant/components/nzbget/__init__.py
+++ b/homeassistant/components/nzbget/__init__.py
@@ -37,7 +37,7 @@
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["sensor"]
+PLATFORMS = ["sensor", "switch"]
CONFIG_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py
index dfed7a9bfeece8..f593eeb0729c4c 100644
--- a/homeassistant/components/nzbget/config_flow.py
+++ b/homeassistant/components/nzbget/config_flow.py
@@ -38,8 +38,8 @@ def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
"""
nzbget_api = NZBGetAPI(
data[CONF_HOST],
- data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None,
- data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None,
+ data.get(CONF_USERNAME),
+ data.get(CONF_PASSWORD),
data[CONF_SSL],
data[CONF_VERIFY_SSL],
data[CONF_PORT],
diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py
index 8892475bc098b9..9a76d802bdd3a6 100644
--- a/homeassistant/components/nzbget/coordinator.py
+++ b/homeassistant/components/nzbget/coordinator.py
@@ -29,8 +29,8 @@ def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict):
"""Initialize global NZBGet data updater."""
self.nzbget = NZBGetAPI(
config[CONF_HOST],
- config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None,
- config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None,
+ config.get(CONF_USERNAME),
+ config.get(CONF_PASSWORD),
config[CONF_SSL],
config[CONF_VERIFY_SSL],
config[CONF_PORT],
diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py
index ddbc73ca10a63e..b4133e7550d985 100644
--- a/homeassistant/components/nzbget/sensor.py
+++ b/homeassistant/components/nzbget/sensor.py
@@ -64,7 +64,7 @@ async def async_setup_entry(
async_add_entities(sensors)
-class NZBGetSensor(NZBGetEntity, Entity):
+class NZBGetSensor(NZBGetEntity):
"""Representation of a NZBGet sensor."""
def __init__(
diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json
index 5a0c31054a9a71..96049ea936986c 100644
--- a/homeassistant/components/nzbget/strings.json
+++ b/homeassistant/components/nzbget/strings.json
@@ -5,13 +5,13 @@
"user": {
"title": "Connect to NZBGet",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"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%]",
- "ssl": "NZBGet uses a SSL certificate",
- "verify_ssl": "NZBGet uses a proper certificate"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py
new file mode 100644
index 00000000000000..c4ceaab5ded353
--- /dev/null
+++ b/homeassistant/components/nzbget/switch.py
@@ -0,0 +1,72 @@
+"""Support for NZBGet switches."""
+from typing import Callable, List
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_NAME
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import NZBGetEntity
+from .const import DATA_COORDINATOR, DOMAIN
+from .coordinator import NZBGetDataUpdateCoordinator
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], bool], None],
+) -> None:
+ """Set up NZBGet sensor based on a config entry."""
+ coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
+ DATA_COORDINATOR
+ ]
+
+ switches = [
+ NZBGetDownloadSwitch(
+ coordinator,
+ entry.entry_id,
+ entry.data[CONF_NAME],
+ ),
+ ]
+
+ async_add_entities(switches)
+
+
+class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity):
+ """Representation of a NZBGet download switch."""
+
+ def __init__(
+ self,
+ coordinator: NZBGetDataUpdateCoordinator,
+ entry_id: str,
+ entry_name: str,
+ ):
+ """Initialize a new NZBGet switch."""
+ self._unique_id = f"{entry_id}_download"
+
+ super().__init__(
+ coordinator=coordinator,
+ entry_id=entry_id,
+ name=f"{entry_name} Download",
+ )
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID of the switch."""
+ return self._unique_id
+
+ @property
+ def is_on(self):
+ """Return the state of the switch."""
+ return not self.coordinator.data["status"].get("DownloadPaused", False)
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Set downloads to enabled."""
+ await self.hass.async_add_executor_job(self.coordinator.nzbget.resumedownload)
+ await self.coordinator.async_request_refresh()
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Set downloads to paused."""
+ await self.hass.async_add_executor_job(self.coordinator.nzbget.pausedownload)
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/nzbget/translations/ca.json b/homeassistant/components/nzbget/translations/ca.json
index 535b56a3768de7..b567d77bcb3a92 100644
--- a/homeassistant/components/nzbget/translations/ca.json
+++ b/homeassistant/components/nzbget/translations/ca.json
@@ -16,9 +16,9 @@
"name": "Nom",
"password": "Contrasenya",
"port": "Port",
- "ssl": "NZBGet utilitza un certificat SSL",
+ "ssl": "Utilitza un certificat SSL",
"username": "Nom d'usuari",
- "verify_ssl": "NZBGet utilitza un certificat adequat"
+ "verify_ssl": "Verifica el certificat SSL"
},
"title": "Connexi\u00f3 amb NZBGet"
}
diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json
new file mode 100644
index 00000000000000..6aa89d2e62ec3e
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/de.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "flow_title": "NZBGet: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Aktualisierungsh\u00e4ufigkeit (Sekunden)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/el.json b/homeassistant/components/nzbget/translations/el.json
new file mode 100644
index 00000000000000..25febfd7c8a34c
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\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"
+ },
+ "flow_title": "NZBGet: {\u03cc\u03bd\u03bf\u03bc\u03b1}"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/en.json b/homeassistant/components/nzbget/translations/en.json
index c410d4e487493c..d9a6273c27cffb 100644
--- a/homeassistant/components/nzbget/translations/en.json
+++ b/homeassistant/components/nzbget/translations/en.json
@@ -16,9 +16,9 @@
"name": "Name",
"password": "Password",
"port": "Port",
- "ssl": "NZBGet uses a SSL certificate",
+ "ssl": "Uses an SSL certificate",
"username": "Username",
- "verify_ssl": "NZBGet uses a proper certificate"
+ "verify_ssl": "Verify SSL certificate"
},
"title": "Connect to NZBGet"
}
diff --git a/homeassistant/components/nzbget/translations/it.json b/homeassistant/components/nzbget/translations/it.json
index 31438f9d55b689..88bea29987b3f4 100644
--- a/homeassistant/components/nzbget/translations/it.json
+++ b/homeassistant/components/nzbget/translations/it.json
@@ -16,9 +16,9 @@
"name": "Nome",
"password": "Password",
"port": "Porta",
- "ssl": "NZBGet utilizza un certificato SSL",
+ "ssl": "Utilizza un certificato SSL",
"username": "Nome utente",
- "verify_ssl": "NZBGet utilizza un certificato proprio"
+ "verify_ssl": "Verificare il certificato SSL"
},
"title": "Connettiti a NZBGet"
}
diff --git a/homeassistant/components/nzbget/translations/ko.json b/homeassistant/components/nzbget/translations/ko.json
new file mode 100644
index 00000000000000..ea9108b936710b
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/ko.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d"
+ },
+ "flow_title": "NZBGet : {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "password": "\uc554\ud638",
+ "port": "\ud3ec\ud2b8",
+ "ssl": "NZBGet\uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.",
+ "username": "\uc0ac\uc6a9\uc790\uba85",
+ "verify_ssl": "NZBGet\uc740 \uc801\uc808\ud55c \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4."
+ },
+ "title": "NZBGet\uc5d0 \uc5f0\uacb0"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/lb.json b/homeassistant/components/nzbget/translations/lb.json
new file mode 100644
index 00000000000000..5da36a5d859583
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/lb.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "Onerwaarte Feeler"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun"
+ },
+ "flow_title": "NZBGet: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Numm",
+ "password": "Passwuert",
+ "port": "Port",
+ "ssl": "NZBGet benotzt een SSL Zertifikat",
+ "username": "Benotzernumm",
+ "verify_ssl": "NZBGet benotzt ee g\u00ebltegen SSL Zertifikat"
+ },
+ "title": "Mat NZBGet verbannen"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalle vun de Mise \u00e0 jour (sekonnen)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json
new file mode 100644
index 00000000000000..472952ad8b14e5
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/nl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Naam",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/no.json b/homeassistant/components/nzbget/translations/no.json
index e8230da6f2db8c..3afef3773836a4 100644
--- a/homeassistant/components/nzbget/translations/no.json
+++ b/homeassistant/components/nzbget/translations/no.json
@@ -16,9 +16,9 @@
"name": "Navn",
"password": "Passord",
"port": "Port",
- "ssl": "NZBGet bruker et SSL-sertifikat",
+ "ssl": "Bruker et SSL-sertifikat",
"username": "Brukernavn",
- "verify_ssl": "NZBGet bruker et riktig sertifikat"
+ "verify_ssl": "Verifisere SSL-sertifikat"
},
"title": "Koble til NZBGet"
}
diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json
index a5bd1b5cdcb0ec..e01c59fe2a7927 100644
--- a/homeassistant/components/nzbget/translations/pl.json
+++ b/homeassistant/components/nzbget/translations/pl.json
@@ -1,7 +1,12 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
},
"flow_title": "NZBGet: {name}",
"step": {
@@ -12,6 +17,16 @@
"password": "Has\u0142o",
"port": "Port",
"username": "Nazwa u\u017cytkownika"
+ },
+ "title": "Po\u0142\u0105czenie z NZBGet"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w sekundach)"
}
}
}
diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json
index 93e44307ab8543..daab5c87ef72a8 100644
--- a/homeassistant/components/nzbget/translations/ru.json
+++ b/homeassistant/components/nzbget/translations/ru.json
@@ -16,9 +16,9 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "NZBGet \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"username": "\u041b\u043e\u0433\u0438\u043d",
- "verify_ssl": "NZBGet \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "NZBGet"
}
diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json
index d2fc50ac26acc7..e2c3c8ea8207d4 100644
--- a/homeassistant/components/nzbget/translations/zh-Hant.json
+++ b/homeassistant/components/nzbget/translations/zh-Hant.json
@@ -16,9 +16,9 @@
"name": "\u540d\u7a31",
"password": "\u5bc6\u78bc",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "NZBGet \u4f7f\u7528 SSL \u8a8d\u8b49",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"username": "\u4f7f\u7528\u8005\u540d\u7a31",
- "verify_ssl": "NZBGet \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"title": "\u9023\u7dda\u81f3 NZBGet"
}
diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py
new file mode 100644
index 00000000000000..ff4dd93a0e1537
--- /dev/null
+++ b/homeassistant/components/omnilogic/__init__.py
@@ -0,0 +1,90 @@
+"""The Omnilogic integration."""
+import asyncio
+import logging
+
+from omnilogic import LoginException, OmniLogic, OmniLogicException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+
+from .common import OmniLogicUpdateCoordinator
+from .const import CONF_SCAN_INTERVAL, COORDINATOR, DOMAIN, OMNI_API
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Omnilogic component."""
+ hass.data.setdefault(DOMAIN, {})
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Omnilogic from a config entry."""
+
+ conf = entry.data
+ username = conf[CONF_USERNAME]
+ password = conf[CONF_PASSWORD]
+
+ polling_interval = 6
+ if CONF_SCAN_INTERVAL in conf:
+ polling_interval = conf[CONF_SCAN_INTERVAL]
+
+ session = aiohttp_client.async_get_clientsession(hass)
+
+ api = OmniLogic(username, password, session)
+
+ try:
+ await api.connect()
+ await api.get_telemetry_data()
+ except LoginException as error:
+ _LOGGER.error("Login Failed: %s", error)
+ return False
+ except OmniLogicException as error:
+ _LOGGER.debug("OmniLogic API error: %s", error)
+ raise ConfigEntryNotReady from error
+
+ coordinator = OmniLogicUpdateCoordinator(
+ hass=hass,
+ api=api,
+ name="Omnilogic",
+ polling_interval=polling_interval,
+ )
+ await coordinator.async_refresh()
+
+ if not coordinator.last_update_success:
+ raise ConfigEntryNotReady
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ COORDINATOR: coordinator,
+ OMNI_API: api,
+ }
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py
new file mode 100644
index 00000000000000..791d81b6757a53
--- /dev/null
+++ b/homeassistant/components/omnilogic/common.py
@@ -0,0 +1,157 @@
+"""Common classes and elements for Omnilogic Integration."""
+
+from datetime import timedelta
+import logging
+
+from omnilogic import OmniLogicException
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import (
+ ALL_ITEM_KINDS,
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class OmniLogicUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching update data from single endpoint."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ api: str,
+ name: str,
+ polling_interval: int,
+ ):
+ """Initialize the global Omnilogic data updater."""
+ self.api = api
+
+ super().__init__(
+ hass=hass,
+ logger=_LOGGER,
+ name=name,
+ update_interval=timedelta(seconds=polling_interval),
+ )
+
+ async def _async_update_data(self):
+ """Fetch data from OmniLogic."""
+ try:
+ data = await self.api.get_telemetry_data()
+
+ except OmniLogicException as error:
+ raise UpdateFailed(f"Error updating from OmniLogic: {error}") from error
+
+ parsed_data = {}
+
+ def get_item_data(item, item_kind, current_id, data):
+ """Get data per kind of Omnilogic API item."""
+ if isinstance(item, list):
+ for single_item in item:
+ data = get_item_data(single_item, item_kind, current_id, data)
+
+ if "systemId" in item:
+ system_id = item["systemId"]
+ current_id = current_id + (item_kind, system_id)
+ data[current_id] = item
+
+ for kind in ALL_ITEM_KINDS:
+ if kind in item:
+ data = get_item_data(item[kind], kind, current_id, data)
+
+ return data
+
+ parsed_data = get_item_data(data, "Backyard", (), parsed_data)
+
+ return parsed_data
+
+
+class OmniLogicEntity(CoordinatorEntity):
+ """Defines the base OmniLogic entity."""
+
+ def __init__(
+ self,
+ coordinator: OmniLogicUpdateCoordinator,
+ kind: str,
+ name: str,
+ item_id: tuple,
+ icon: str,
+ ):
+ """Initialize the OmniLogic Entity."""
+ super().__init__(coordinator)
+
+ bow_id = None
+ entity_data = coordinator.data[item_id]
+
+ backyard_id = item_id[:2]
+ if len(item_id) == 6:
+ bow_id = item_id[:4]
+
+ msp_system_id = coordinator.data[backyard_id]["systemId"]
+ entity_friendly_name = f"{coordinator.data[backyard_id]['BackyardName']} "
+ unique_id = f"{msp_system_id}"
+
+ if bow_id is not None:
+ unique_id = f"{unique_id}_{coordinator.data[bow_id]['systemId']}"
+ entity_friendly_name = (
+ f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} "
+ )
+
+ unique_id = f"{unique_id}_{coordinator.data[item_id]['systemId']}_{kind}"
+
+ if entity_data.get("Name") is not None:
+ entity_friendly_name = f"{entity_friendly_name} {entity_data['Name']}"
+
+ entity_friendly_name = f"{entity_friendly_name} {name}"
+
+ unique_id = unique_id.replace(" ", "_")
+
+ self._kind = kind
+ self._name = entity_friendly_name
+ self._unique_id = unique_id
+ self._item_id = item_id
+ self._icon = icon
+ self._attrs = {}
+ self._msp_system_id = msp_system_id
+ self._backyard_name = coordinator.data[backyard_id]["BackyardName"]
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique, Home Assistant friendly identifier for this entity."""
+ return self._unique_id
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return the icon for the entity."""
+ return self._icon
+
+ @property
+ def device_state_attributes(self):
+ """Return the attributes."""
+ return self._attrs
+
+ @property
+ def device_info(self):
+ """Define the device as back yard/MSP System."""
+
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._msp_system_id)},
+ ATTR_NAME: self._backyard_name,
+ ATTR_MANUFACTURER: "Hayward",
+ ATTR_MODEL: "OmniLogic",
+ }
diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py
new file mode 100644
index 00000000000000..641ec5a8d94bfd
--- /dev/null
+++ b/homeassistant/components/omnilogic/config_flow.py
@@ -0,0 +1,95 @@
+"""Config flow for Omnilogic integration."""
+import logging
+
+from omnilogic import LoginException, OmniLogic, OmniLogicException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+
+from .const import CONF_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Omnilogic."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+
+ config_entry = self.hass.config_entries.async_entries(DOMAIN)
+ if config_entry:
+ return self.async_abort(reason="single_instance_allowed")
+
+ errors = {}
+
+ if user_input is not None:
+ username = user_input[CONF_USERNAME]
+ password = user_input[CONF_PASSWORD]
+
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ omni = OmniLogic(username, password, session)
+
+ try:
+ await omni.connect()
+ except LoginException:
+ errors["base"] = "invalid_auth"
+ except OmniLogicException:
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(user_input["username"])
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(title="Omnilogic", data=user_input)
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ errors=errors,
+ )
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle Omnilogic client options."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage options."""
+
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_SCAN_INTERVAL,
+ default=6,
+ ): int,
+ }
+ ),
+ )
diff --git a/homeassistant/components/omnilogic/const.py b/homeassistant/components/omnilogic/const.py
new file mode 100644
index 00000000000000..a57ef2b062a928
--- /dev/null
+++ b/homeassistant/components/omnilogic/const.py
@@ -0,0 +1,29 @@
+"""Constants for the Omnilogic integration."""
+
+DOMAIN = "omnilogic"
+CONF_SCAN_INTERVAL = "polling_interval"
+COORDINATOR = "coordinator"
+OMNI_API = "omni_api"
+ATTR_IDENTIFIERS = "identifiers"
+ATTR_MANUFACTURER = "manufacturer"
+ATTR_MODEL = "model"
+
+PUMP_TYPES = {
+ "FMT_VARIABLE_SPEED_PUMP": "VARIABLE",
+ "FMT_SINGLE_SPEED": "SINGLE",
+ "FMT_DUAL_SPEED": "DUAL",
+ "PMP_VARIABLE_SPEED_PUMP": "VARIABLE",
+ "PMP_SINGLE_SPEED": "SINGLE",
+ "PMP_DUAL_SPEED": "DUAL",
+}
+
+ALL_ITEM_KINDS = {
+ "BOWS",
+ "Filter",
+ "Heater",
+ "Chlorinator",
+ "CSAD",
+ "Lights",
+ "Relays",
+ "Pumps",
+}
diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json
new file mode 100644
index 00000000000000..468b48d620a9d6
--- /dev/null
+++ b/homeassistant/components/omnilogic/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "omnilogic",
+ "name": "Hayward Omnilogic",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/omnilogic",
+ "requirements": ["omnilogic==0.4.0"],
+ "codeowners": ["@oliver84","@djtimca","@gentoosu"]
+}
diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py
new file mode 100644
index 00000000000000..f4bb0f45d5ed8c
--- /dev/null
+++ b/homeassistant/components/omnilogic/sensor.py
@@ -0,0 +1,356 @@
+"""Definition and setup of the Omnilogic Sensors for Home Assistant."""
+
+import logging
+
+from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE
+from homeassistant.const import (
+ CONCENTRATION_PARTS_PER_MILLION,
+ MASS_GRAMS,
+ PERCENTAGE,
+ TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
+ VOLUME_LITERS,
+)
+
+from .common import OmniLogicEntity, OmniLogicUpdateCoordinator
+from .const import COORDINATOR, DOMAIN, PUMP_TYPES
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the sensor platform."""
+
+ coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
+ entities = []
+
+ for item_id, item in coordinator.data.items():
+ id_len = len(item_id)
+ item_kind = item_id[-2]
+ entity_settings = SENSOR_TYPES.get((id_len, item_kind))
+
+ if not entity_settings:
+ continue
+
+ for entity_setting in entity_settings:
+ for state_key, entity_class in entity_setting["entity_classes"].items():
+ if state_key not in item:
+ continue
+
+ guard = False
+ for guard_condition in entity_setting["guard_condition"]:
+ if guard_condition and all(
+ item.get(guard_key) == guard_value
+ for guard_key, guard_value in guard_condition.items()
+ ):
+ guard = True
+
+ if guard:
+ continue
+
+ entity = entity_class(
+ coordinator=coordinator,
+ state_key=state_key,
+ name=entity_setting["name"],
+ kind=entity_setting["kind"],
+ item_id=item_id,
+ device_class=entity_setting["device_class"],
+ icon=entity_setting["icon"],
+ unit=entity_setting["unit"],
+ )
+
+ entities.append(entity)
+
+ async_add_entities(entities)
+
+
+class OmnilogicSensor(OmniLogicEntity):
+ """Defines an Omnilogic sensor entity."""
+
+ def __init__(
+ self,
+ coordinator: OmniLogicUpdateCoordinator,
+ kind: str,
+ name: str,
+ device_class: str,
+ icon: str,
+ unit: str,
+ item_id: tuple,
+ state_key: str,
+ ):
+ """Initialize Entities."""
+ super().__init__(
+ coordinator=coordinator,
+ kind=kind,
+ name=name,
+ item_id=item_id,
+ icon=icon,
+ )
+
+ backyard_id = item_id[:2]
+ unit_type = coordinator.data[backyard_id].get("Unit-of-Measurement")
+
+ self._unit_type = unit_type
+ self._device_class = device_class
+ self._unit = unit
+ self._state_key = state_key
+
+ @property
+ def device_class(self):
+ """Return the device class of the entity."""
+ return self._device_class
+
+ @property
+ def unit_of_measurement(self):
+ """Return the right unit of measure."""
+ return self._unit
+
+
+class OmniLogicTemperatureSensor(OmnilogicSensor):
+ """Define an OmniLogic Temperature (Air/Water) Sensor."""
+
+ @property
+ def state(self):
+ """Return the state for the temperature sensor."""
+ sensor_data = self.coordinator.data[self._item_id][self._state_key]
+
+ hayward_state = sensor_data
+ hayward_unit_of_measure = TEMP_FAHRENHEIT
+ state = sensor_data
+
+ if self._unit_type == "Metric":
+ hayward_state = round((hayward_state - 32) * 5 / 9, 1)
+ hayward_unit_of_measure = TEMP_CELSIUS
+
+ if int(sensor_data) == -1:
+ hayward_state = None
+ state = None
+
+ self._attrs["hayward_temperature"] = hayward_state
+ self._attrs["hayward_unit_of_measure"] = hayward_unit_of_measure
+
+ self._unit = TEMP_FAHRENHEIT
+
+ return state
+
+
+class OmniLogicPumpSpeedSensor(OmnilogicSensor):
+ """Define an OmniLogic Pump Speed Sensor."""
+
+ @property
+ def state(self):
+ """Return the state for the pump speed sensor."""
+
+ pump_type = PUMP_TYPES[self.coordinator.data[self._item_id]["Filter-Type"]]
+ pump_speed = self.coordinator.data[self._item_id][self._state_key]
+
+ if pump_type == "VARIABLE":
+ self._unit = PERCENTAGE
+ state = pump_speed
+ elif pump_type == "DUAL":
+ if pump_speed == 0:
+ state = "off"
+ elif pump_speed == self.coordinator.data[self._item_id].get(
+ "Min-Pump-Speed"
+ ):
+ state = "low"
+ elif pump_speed == self.coordinator.data[self._item_id].get(
+ "Max-Pump-Speed"
+ ):
+ state = "high"
+
+ self._attrs["pump_type"] = pump_type
+
+ return state
+
+
+class OmniLogicSaltLevelSensor(OmnilogicSensor):
+ """Define an OmniLogic Salt Level Sensor."""
+
+ @property
+ def state(self):
+ """Return the state for the salt level sensor."""
+
+ salt_return = self.coordinator.data[self._item_id][self._state_key]
+ unit_of_measurement = self._unit
+
+ if self._unit_type == "Metric":
+ salt_return = round(salt_return / 1000, 2)
+ unit_of_measurement = f"{MASS_GRAMS}/{VOLUME_LITERS}"
+
+ self._unit = unit_of_measurement
+
+ return salt_return
+
+
+class OmniLogicChlorinatorSensor(OmnilogicSensor):
+ """Define an OmniLogic Chlorinator Sensor."""
+
+ @property
+ def state(self):
+ """Return the state for the chlorinator sensor."""
+ state = self.coordinator.data[self._item_id][self._state_key]
+
+ return state
+
+
+class OmniLogicPHSensor(OmnilogicSensor):
+ """Define an OmniLogic pH Sensor."""
+
+ @property
+ def state(self):
+ """Return the state for the pH sensor."""
+
+ ph_state = self.coordinator.data[self._item_id][self._state_key]
+
+ if ph_state == 0:
+ ph_state = None
+
+ return ph_state
+
+
+class OmniLogicORPSensor(OmnilogicSensor):
+ """Define an OmniLogic ORP Sensor."""
+
+ def __init__(
+ self,
+ coordinator: OmniLogicUpdateCoordinator,
+ state_key: str,
+ name: str,
+ kind: str,
+ item_id: tuple,
+ device_class: str,
+ icon: str,
+ unit: str,
+ ):
+ """Initialize the sensor."""
+ super().__init__(
+ coordinator=coordinator,
+ kind=kind,
+ name=name,
+ device_class=device_class,
+ icon=icon,
+ unit=unit,
+ item_id=item_id,
+ state_key=state_key,
+ )
+
+ @property
+ def state(self):
+ """Return the state for the ORP sensor."""
+
+ orp_state = self.coordinator.data[self._item_id][self._state_key]
+
+ if orp_state == -1:
+ orp_state = None
+
+ return orp_state
+
+
+SENSOR_TYPES = {
+ (2, "Backyard"): [
+ {
+ "entity_classes": {"airTemp": OmniLogicTemperatureSensor},
+ "name": "Air Temperature",
+ "kind": "air_temperature",
+ "device_class": DEVICE_CLASS_TEMPERATURE,
+ "icon": None,
+ "unit": TEMP_FAHRENHEIT,
+ "guard_condition": [{}],
+ },
+ ],
+ (4, "BOWS"): [
+ {
+ "entity_classes": {"waterTemp": OmniLogicTemperatureSensor},
+ "name": "Water Temperature",
+ "kind": "water_temperature",
+ "device_class": DEVICE_CLASS_TEMPERATURE,
+ "icon": None,
+ "unit": TEMP_FAHRENHEIT,
+ "guard_condition": [{}],
+ },
+ ],
+ (6, "Filter"): [
+ {
+ "entity_classes": {"filterSpeed": OmniLogicPumpSpeedSensor},
+ "name": "Speed",
+ "kind": "filter_pump_speed",
+ "device_class": None,
+ "icon": "mdi:speedometer",
+ "unit": PERCENTAGE,
+ "guard_condition": [
+ {"Type": "FMT_SINGLE_SPEED"},
+ ],
+ },
+ ],
+ (6, "Pumps"): [
+ {
+ "entity_classes": {"pumpSpeed": OmniLogicPumpSpeedSensor},
+ "name": "Pump Speed",
+ "kind": "pump_speed",
+ "device_class": None,
+ "icon": "mdi:speedometer",
+ "unit": PERCENTAGE,
+ "guard_condition": [
+ {"Type": "PMP_SINGLE_SPEED"},
+ ],
+ },
+ ],
+ (6, "Chlorinator"): [
+ {
+ "entity_classes": {"Timed-Percent": OmniLogicChlorinatorSensor},
+ "name": "Setting",
+ "kind": "chlorinator",
+ "device_class": None,
+ "icon": "mdi:gauge",
+ "unit": PERCENTAGE,
+ "guard_condition": [
+ {
+ "Shared-Type": "BOW_SHARED_EQUIPMENT",
+ "status": "0",
+ },
+ {
+ "operatingMode": "2",
+ },
+ ],
+ },
+ {
+ "entity_classes": {"avgSaltLevel": OmniLogicSaltLevelSensor},
+ "name": "Salt Level",
+ "kind": "salt_level",
+ "device_class": None,
+ "icon": "mdi:gauge",
+ "unit": CONCENTRATION_PARTS_PER_MILLION,
+ "guard_condition": [
+ {
+ "Shared-Type": "BOW_SHARED_EQUIPMENT",
+ "status": "0",
+ },
+ ],
+ },
+ ],
+ (6, "CSAD"): [
+ {
+ "entity_classes": {"ph": OmniLogicPHSensor},
+ "name": "pH",
+ "kind": "csad_ph",
+ "device_class": None,
+ "icon": "mdi:gauge",
+ "unit": "pH",
+ "guard_condition": [
+ {"ph": ""},
+ ],
+ },
+ {
+ "entity_classes": {"orp": OmniLogicORPSensor},
+ "name": "ORP",
+ "kind": "csad_orp",
+ "device_class": None,
+ "icon": "mdi:gauge",
+ "unit": "mV",
+ "guard_condition": [
+ {"orp": ""},
+ ],
+ },
+ ],
+}
diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json
new file mode 100644
index 00000000000000..285bc29b802c9e
--- /dev/null
+++ b/homeassistant/components/omnilogic/strings.json
@@ -0,0 +1,30 @@
+{
+ "title": "Omnilogic",
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "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": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Polling interval (in seconds)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json
new file mode 100644
index 00000000000000..53c8755dd36934
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/ca.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Interval d'escaneig (segons)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json
new file mode 100644
index 00000000000000..c400283458932d
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/de.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/el.json b/homeassistant/components/omnilogic/translations/el.json
new file mode 100644
index 00000000000000..fbe77ec16c0ed4
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/el.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "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": {
+ "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7",
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 (\u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json
new file mode 100644
index 00000000000000..858cfe31323d3d
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/en.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Polling interval (in seconds)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json
new file mode 100644
index 00000000000000..849cd73b40f73d
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/es.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Intervalo de sondeo (en segundos)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json
new file mode 100644
index 00000000000000..41abe3283eb172
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/et.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus",
+ "invalid_auth": "Tuvastamise viga",
+ "unknown": "Tundmatu viga"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "P\u00e4ringute intervall (sekundites)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/fr.json b/homeassistant/components/omnilogic/translations/fr.json
new file mode 100644
index 00000000000000..167beb756a8f1d
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/fr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/it.json b/homeassistant/components/omnilogic/translations/it.json
new file mode 100644
index 00000000000000..38ace995177264
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/it.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Intervallo di scansione (in secondi)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/ko.json b/homeassistant/components/omnilogic/translations/ko.json
new file mode 100644
index 00000000000000..686ca520bff1db
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/ko.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\uc554\ud638",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9(\ucd08)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/lb.json b/homeassistant/components/omnilogic/translations/lb.json
new file mode 100644
index 00000000000000..22f3cb54a6ed0d
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/lb.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "\u00a7",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Intervall vun den Offroen (sekonnen)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json
new file mode 100644
index 00000000000000..2f7e9cfbd12e4c
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/nl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/no.json b/homeassistant/components/omnilogic/translations/no.json
new file mode 100644
index 00000000000000..ebadb3a40e453a
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/no.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Avstemningsintervall (i sekunder)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/pl.json b/homeassistant/components/omnilogic/translations/pl.json
new file mode 100644
index 00000000000000..10cbbdb12b792f
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/pl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w sekundach)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json
new file mode 100644
index 00000000000000..3b05c74695df26
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/ru.json
@@ -0,0 +1,30 @@
+{
+ "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."
+ },
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u041b\u043e\u0433\u0438\u043d"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json
new file mode 100644
index 00000000000000..335e26ced8cf23
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/zh-Hant.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u79d2\uff09"
+ }
+ }
+ }
+ },
+ "title": "Omnilogic"
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json
index 81e88e99edb23f..e2fb8e084b83b8 100644
--- a/homeassistant/components/onboarding/manifest.json
+++ b/homeassistant/components/onboarding/manifest.json
@@ -2,7 +2,16 @@
"domain": "onboarding",
"name": "Home Assistant Onboarding",
"documentation": "https://www.home-assistant.io/integrations/onboarding",
- "dependencies": ["auth", "http", "person"],
- "codeowners": ["@home-assistant/core"],
+ "after_dependencies": [
+ "hassio"
+ ],
+ "dependencies": [
+ "auth",
+ "http",
+ "person"
+ ],
+ "codeowners": [
+ "@home-assistant/core"
+ ],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py
index a2a4fb15fd74e4..0faf099b9bf0fb 100644
--- a/homeassistant/components/onboarding/views.py
+++ b/homeassistant/components/onboarding/views.py
@@ -159,6 +159,14 @@ async def post(self, request):
"met", context={"source": "onboarding"}
)
+ if (
+ hass.components.hassio.is_hassio()
+ and "raspberrypi" in hass.components.hassio.get_core_info()["machine"]
+ ):
+ await hass.config_entries.flow.async_init(
+ "rpi_power", context={"source": "onboarding"}
+ )
+
return self.json({})
diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py
index ac5d139337814d..21dc0c2ead351f 100644
--- a/homeassistant/components/onewire/__init__.py
+++ b/homeassistant/components/onewire/__init__.py
@@ -1 +1 @@
-"""The onewire component."""
+"""The 1-Wire component."""
diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py
new file mode 100644
index 00000000000000..af68135af1090c
--- /dev/null
+++ b/homeassistant/components/onewire/const.py
@@ -0,0 +1,12 @@
+"""Constants for 1-Wire component."""
+CONF_MOUNT_DIR = "mount_dir"
+CONF_NAMES = "names"
+
+DEFAULT_OWSERVER_PORT = 4304
+DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/"
+
+DOMAIN = "onewire"
+
+SUPPORTED_PLATFORMS = [
+ "sensor",
+]
diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py
index 148c596e130bb8..2ac00c814b2fea 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -12,6 +12,7 @@
CONF_HOST,
CONF_PORT,
ELECTRICAL_CURRENT_AMPERE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
VOLT,
@@ -19,12 +20,15 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-_LOGGER = logging.getLogger(__name__)
+from .const import (
+ CONF_MOUNT_DIR,
+ CONF_NAMES,
+ DEFAULT_OWSERVER_PORT,
+ DEFAULT_SYSBUS_MOUNT_DIR,
+)
-CONF_MOUNT_DIR = "mount_dir"
-CONF_NAMES = "names"
+_LOGGER = logging.getLogger(__name__)
-DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/"
DEVICE_SENSORS = {
# Family : { SensorType: owfs path }
"10": {"temperature": "temperature"},
@@ -33,6 +37,10 @@
"26": {
"temperature": "temperature",
"humidity": "humidity",
+ "humidity_hih3600": "HIH3600/humidity",
+ "humidity_hih4000": "HIH4000/humidity",
+ "humidity_hih5030": "HIH5030/humidity",
+ "humidity_htm1735": "HTM1735/humidity",
"pressure": "B1-R1-A/pressure",
"illuminance": "S3-R1-A/illuminance",
"voltage_VAD": "VAD",
@@ -68,9 +76,13 @@
# SensorType: [ Measured unit, Unit ]
"temperature": ["temperature", TEMP_CELSIUS],
"humidity": ["humidity", PERCENTAGE],
+ "humidity_hih3600": ["humidity", PERCENTAGE],
+ "humidity_hih4000": ["humidity", PERCENTAGE],
+ "humidity_hih5030": ["humidity", PERCENTAGE],
+ "humidity_htm1735": ["humidity", PERCENTAGE],
"humidity_raw": ["humidity", PERCENTAGE],
"pressure": ["pressure", "mb"],
- "illuminance": ["illuminance", "lux"],
+ "illuminance": ["illuminance", LIGHT_LUX],
"wetness_0": ["wetness", PERCENTAGE],
"wetness_1": ["wetness", PERCENTAGE],
"wetness_2": ["wetness", PERCENTAGE],
@@ -91,9 +103,9 @@
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAMES): {cv.string: cv.string},
- vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string,
+ vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): cv.string,
vol.Optional(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=4304): cv.port,
+ vol.Optional(CONF_PORT, default=DEFAULT_OWSERVER_PORT): cv.port,
}
)
@@ -107,14 +119,10 @@ def hb_info_from_type(dev_type="std"):
def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the one wire Sensors."""
+ """Set up 1-Wire platform."""
base_dir = config[CONF_MOUNT_DIR]
owport = config[CONF_PORT]
owhost = config.get(CONF_HOST)
- if owhost:
- _LOGGER.debug("Initializing using %s:%s", owhost, owport)
- else:
- _LOGGER.debug("Initializing using %s", base_dir)
devs = []
device_names = {}
@@ -124,6 +132,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# We have an owserver on a remote(or local) host/port
if owhost:
+ _LOGGER.debug("Initializing using %s:%s", owhost, owport)
try:
owproxy = protocol.proxy(host=owhost, port=owport)
devices = owproxy.dir()
@@ -154,7 +163,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
owproxy.read(f"{device}moisture/is_leaf.{s_id}").decode()
)
if is_leaf:
- sensor_key = f"wetness_{id}"
+ sensor_key = f"wetness_{s_id}"
sensor_id = os.path.split(os.path.split(device)[0])[1]
device_file = os.path.join(os.path.split(device)[0], sensor_value)
devs.append(
@@ -167,7 +176,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
# We have a raw GPIO ow sensor on a Pi
- elif base_dir == DEFAULT_MOUNT_DIR:
+ elif base_dir == DEFAULT_SYSBUS_MOUNT_DIR:
+ _LOGGER.debug("Initializing using SysBus %s", base_dir)
for device_family in DEVICE_SENSORS:
for device_folder in glob(os.path.join(base_dir, f"{device_family}[.-]*")):
sensor_id = os.path.split(device_folder)[1]
@@ -182,6 +192,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# We have an owfs mounted
else:
+ _LOGGER.debug("Initializing using OWFS %s", base_dir)
for family_file_path in glob(os.path.join(base_dir, "*", "family")):
with open(family_file_path) as family_file:
family = family_file.read()
@@ -213,7 +224,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class OneWire(Entity):
- """Implementation of an One wire Sensor."""
+ """Implementation of a 1-Wire sensor."""
def __init__(self, name, device_file, sensor_type):
"""Initialize the sensor."""
@@ -258,10 +269,10 @@ def unique_id(self) -> str:
class OneWireProxy(OneWire):
- """Implementation of a One wire Sensor through owserver."""
+ """Implementation of a 1-Wire sensor through owserver."""
def __init__(self, name, device_file, sensor_type, owproxy):
- """Initialize the onewire sensor via owserver."""
+ """Initialize the sensor."""
super().__init__(name, device_file, sensor_type)
self._owproxy = owproxy
@@ -287,7 +298,7 @@ def update(self):
class OneWireDirect(OneWire):
- """Implementation of an One wire Sensor directly connected to RPI GPIO."""
+ """Implementation of a 1-Wire sensor directly connected to RPI GPIO."""
def update(self):
"""Get the latest data from the device."""
@@ -305,7 +316,7 @@ def update(self):
class OneWireOWFS(OneWire):
- """Implementation of an One wire Sensor through owfs."""
+ """Implementation of a 1-Wire sensor through owfs."""
def update(self):
"""Get the latest data from the device."""
diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py
index 964c7a70a6ddb7..cf92f3df3ba35b 100644
--- a/homeassistant/components/onvif/__init__.py
+++ b/homeassistant/components/onvif/__init__.py
@@ -17,6 +17,7 @@
EVENT_HOMEASSISTANT_STOP,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
+ HTTP_UNAUTHORIZED,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -138,7 +139,7 @@ def _get():
try:
response = await hass.async_add_executor_job(_get)
- if response.status_code == 401:
+ if response.status_code == HTTP_UNAUTHORIZED:
return HTTP_BASIC_AUTHENTICATION
return HTTP_DIGEST_AUTHENTICATION
diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py
index cbe6d03fa68b25..ccb301a455fd3e 100644
--- a/homeassistant/components/onvif/config_flow.py
+++ b/homeassistant/components/onvif/config_flow.py
@@ -262,7 +262,7 @@ async def async_step_profiles(self, user_input=None):
return self.async_abort(reason="onvif_error")
except Fault:
- errors["base"] = "connection_failed"
+ errors["base"] = "cannot_connect"
return self.async_show_form(step_id="auth", errors=errors)
diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json
index b6ae9b98d9abb7..dac8ef8647de2d 100644
--- a/homeassistant/components/onvif/strings.json
+++ b/homeassistant/components/onvif/strings.json
@@ -1,14 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "ONVIF device is already configured.",
- "already_in_progress": "Config flow for ONVIF device is already in progress.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"onvif_error": "Error setting up ONVIF device. Check logs for more information.",
"no_h264": "There were no H264 streams available. Check the profile configuration on your device.",
"no_mac": "Could not configure unique ID for ONVIF device."
},
"error": {
- "connection_failed": "Could not connect to ONVIF service with provided credentials."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
@@ -23,7 +23,7 @@
},
"manual_input": {
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
diff --git a/homeassistant/components/onvif/translations/ca.json b/homeassistant/components/onvif/translations/ca.json
index a0362abbb8fa3e..99701f4020ba12 100644
--- a/homeassistant/components/onvif/translations/ca.json
+++ b/homeassistant/components/onvif/translations/ca.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Dispositiu ONVIF ja configurat.",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ONVIF ja est\u00e0 en curs.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"no_h264": "No s'han torbat fluxos (streams) H264 disponibles. Comporva la configuraci\u00f3 de perfil en el dispositiu.",
"no_mac": "No s'ha pogut configurar un ID \u00fanic pel dispositiu ONVIF.",
"onvif_error": "Error durant la configuraci\u00f3 del dispositiu ONVIF. Consulta els registres per a m\u00e9s informaci\u00f3."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_failed": "No s'ha pogut connectar al servei ONVIF amb les credencials proporcionades."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json
index a20b1fcb7e4826..e414d5a6d8dbb9 100644
--- a/homeassistant/components/onvif/translations/en.json
+++ b/homeassistant/components/onvif/translations/en.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "ONVIF device is already configured.",
- "already_in_progress": "Config flow for ONVIF device is already in progress.",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"no_h264": "There were no H264 streams available. Check the profile configuration on your device.",
"no_mac": "Could not configure unique ID for ONVIF device.",
"onvif_error": "Error setting up ONVIF device. Check logs for more information."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_failed": "Could not connect to ONVIF service with provided credentials."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json
index af283fe038bedf..9cb854b5988f62 100644
--- a/homeassistant/components/onvif/translations/es.json
+++ b/homeassistant/components/onvif/translations/es.json
@@ -8,6 +8,7 @@
"onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Revise los registros para m\u00e1s informaci\u00f3n."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_failed": "No se pudo conectar al servicio ONVIF con las credenciales proporcionadas."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/et.json b/homeassistant/components/onvif/translations/et.json
new file mode 100644
index 00000000000000..8f0eb1c05f1ebc
--- /dev/null
+++ b/homeassistant/components/onvif/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json
index 374c65b99a1118..2183945d680951 100644
--- a/homeassistant/components/onvif/translations/it.json
+++ b/homeassistant/components/onvif/translations/it.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo ONVIF \u00e8 gi\u00e0 configurato.",
- "already_in_progress": "Il flusso di configurazione per il dispositivo ONVIF \u00e8 gi\u00e0 in corso.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"no_h264": "Non c'erano flussi H264 disponibili. Controllare la configurazione del profilo sul dispositivo.",
"no_mac": "Impossibile configurare l'ID univoco per il dispositivo ONVIF.",
"onvif_error": "Errore durante la configurazione del dispositivo ONVIF. Controllare i registri per ulteriori informazioni."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_failed": "Impossibile connettersi al servizio ONVIF con le credenziali fornite."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json
index 4f605a518d765b..13cff6e14e2fc4 100644
--- a/homeassistant/components/onvif/translations/no.json
+++ b/homeassistant/components/onvif/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ONVIF-enheten er allerede konfigurert.",
- "already_in_progress": "Konfigurasjonsflyt for ONVIF-enhet p\u00e5g\u00e5r allerede.",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"no_h264": "Det var ingen H264-str\u00f8mmer tilgjengelig. Sjekk profilkonfigurasjonen p\u00e5 enheten din.",
"no_mac": "Kunne ikke konfigurere unik ID for ONVIF-enhet.",
"onvif_error": "Feil ved konfigurering av ONVIF-enhet. Sjekk logger for mer informasjon."
diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json
index d60d45c746f78d..57389baafbd270 100644
--- a/homeassistant/components/onvif/translations/pl.json
+++ b/homeassistant/components/onvif/translations/pl.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Proces konfiguracji dla urz\u0105dzenia ONVIF jest ju\u017c w toku.",
"no_h264": "Nie by\u0142o dost\u0119pnych \u017cadnych strumieni H264. Sprawd\u017a konfiguracj\u0119 profilu w swoim urz\u0105dzeniu.",
"no_mac": "Nie mo\u017cna utworzy\u0107 unikalnego identyfikatora urz\u0105dzenia ONVIF.",
"onvif_error": "Wyst\u0105pi\u0142 b\u0142\u0105d podczas konfigurowania urz\u0105dzenia ONVIF. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_failed": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z us\u0142ug\u0105 ONVIF z podanymi po\u015bwiadczeniami."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json
index 84ff17750803f7..7d6cd66f8cd9c2 100644
--- a/homeassistant/components/onvif/translations/ru.json
+++ b/homeassistant/components/onvif/translations/ru.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"no_h264": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043f\u043e\u0442\u043e\u043a\u043e\u0432 H264. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435.",
"no_mac": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
"onvif_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
},
"error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u043b\u0443\u0436\u0431\u0435 ONVIF \u0441 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u043c\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438."
},
"step": {
diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json
index 794cae79683d8b..c4bac0857a0ebf 100644
--- a/homeassistant/components/onvif/translations/zh-Hant.json
+++ b/homeassistant/components/onvif/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "ONVIF \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002",
- "already_in_progress": "ONVIF \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"no_h264": "\u8a72\u8a2d\u5099\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a2d\u5b9a\u3002",
"no_mac": "\u7121\u6cd5\u70ba ONVIF \u8a2d\u5099\u8a2d\u5b9a\u552f\u4e00 ID\u3002",
"onvif_error": "\u8a2d\u5b9a ONVIF \u8a2d\u5099\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002"
diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py
index c7c96b05872c25..644f2c687b298a 100644
--- a/homeassistant/components/openalpr_local/image_processing.py
+++ b/homeassistant/components/openalpr_local/image_processing.py
@@ -104,9 +104,7 @@ def device_class(self):
@property
def state_attributes(self):
"""Return device specific state attributes."""
- attr = {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles}
-
- return attr
+ return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles}
def process_plates(self, plates, vehicles):
"""Send event with new plates and store data."""
diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py
index b4258a883473ef..f9dbd4272ba184 100644
--- a/homeassistant/components/openhome/media_player.py
+++ b/homeassistant/components/openhome/media_player.py
@@ -187,11 +187,6 @@ def supported_features(self):
"""Flag of features commands that are supported."""
return self._supported_features
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def unique_id(self):
"""Return a unique ID."""
diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py
index 0b696ed933948b..3ff1577c43657b 100644
--- a/homeassistant/components/opentherm_gw/const.py
+++ b/homeassistant/components/opentherm_gw/const.py
@@ -1,9 +1,11 @@
"""Constants for the opentherm_gw integration."""
import pyotgw.vars as gw_vars
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
+ PRESSURE_BAR,
TEMP_CELSIUS,
TIME_HOURS,
TIME_MINUTES,
@@ -23,7 +25,6 @@
DEVICE_CLASS_COLD = "cold"
DEVICE_CLASS_HEAT = "heat"
-DEVICE_CLASS_PROBLEM = "problem"
DOMAIN = "opentherm_gw"
@@ -39,7 +40,6 @@
SERVICE_SET_OAT = "set_outside_temperature"
SERVICE_SET_SB_TEMP = "set_setback_temperature"
-UNIT_BAR = "bar"
UNIT_KW = "kW"
UNIT_L_MIN = f"L/{TIME_MINUTES}"
@@ -152,7 +152,11 @@
"Room Setpoint {}",
],
gw_vars.DATA_REL_MOD_LEVEL: [None, PERCENTAGE, "Relative Modulation Level {}"],
- gw_vars.DATA_CH_WATER_PRESS: [None, UNIT_BAR, "Central Heating Water Pressure {}"],
+ gw_vars.DATA_CH_WATER_PRESS: [
+ None,
+ PRESSURE_BAR,
+ "Central Heating Water Pressure {}",
+ ],
gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate {}"],
gw_vars.DATA_ROOM_SETPOINT_2: [
DEVICE_CLASS_TEMPERATURE,
diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json
index eb074e608ca6be..332b97eb5ee3a6 100644
--- a/homeassistant/components/opentherm_gw/strings.json
+++ b/homeassistant/components/opentherm_gw/strings.json
@@ -3,7 +3,11 @@
"step": {
"init": {
"title": "OpenTherm Gateway",
- "data": { "name": "Name", "device": "Path or URL", "id": "ID" }
+ "data": {
+ "name": "[%key:common::config_flow::data::name%]",
+ "device": "Path or URL",
+ "id": "ID"
+ }
}
},
"error": {
diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json
index 0777b139cf97cb..039a7a9d68912e 100644
--- a/homeassistant/components/openuv/strings.json
+++ b/homeassistant/components/openuv/strings.json
@@ -6,8 +6,8 @@
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"elevation": "Elevation",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json
new file mode 100644
index 00000000000000..aae3ef835bbe20
--- /dev/null
+++ b/homeassistant/components/openuv/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json
index e6113500fbe6b1..dc150a34db84c2 100644
--- a/homeassistant/components/openuv/translations/it.json
+++ b/homeassistant/components/openuv/translations/it.json
@@ -13,7 +13,7 @@
"api_key": "Chiave API",
"elevation": "Altitudine",
"latitude": "Latitudine",
- "longitude": "Longitudine"
+ "longitude": "Logitudine"
},
"title": "Inserisci i tuoi dati"
}
diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json
index 28a43bd10d9616..003f898dfab98f 100644
--- a/homeassistant/components/openuv/translations/pl.json
+++ b/homeassistant/components/openuv/translations/pl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane."
+ },
"error": {
"identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane.",
"invalid_api_key": "Nieprawid\u0142owy klucz API"
diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py
index 365f55c5d44c82..1a50b035db868e 100644
--- a/homeassistant/components/openweathermap/config_flow.py
+++ b/homeassistant/components/openweathermap/config_flow.py
@@ -69,11 +69,11 @@ async def async_step_user(self, user_input=None):
self.hass, user_input[CONF_API_KEY]
)
if not api_online:
- errors["base"] = "auth"
+ errors["base"] = "invalid_api_key"
except UnauthorizedError:
- errors["base"] = "auth"
+ errors["base"] = "invalid_api_key"
except APICallError:
- errors["base"] = "connection"
+ errors["base"] = "cannot_connect"
if not errors:
return self.async_create_entry(
@@ -91,7 +91,7 @@ async def async_step_import(self, import_input=None):
if CONF_LONGITUDE not in config:
config[CONF_LONGITUDE] = self.hass.config.longitude
if CONF_MODE not in config:
- config[CONF_MODE] = DEFAULT_LANGUAGE
+ config[CONF_MODE] = DEFAULT_FORECAST_MODE
if CONF_LANGUAGE not in config:
config[CONF_LANGUAGE] = DEFAULT_LANGUAGE
return await self.async_step_user(config)
diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py
index bc7a428f366575..03ed97d4075ef9 100644
--- a/homeassistant/components/openweathermap/const.py
+++ b/homeassistant/components/openweathermap/const.py
@@ -14,6 +14,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_PA,
SPEED_METERS_PER_SECOND,
@@ -72,7 +73,57 @@
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
]
-LANGUAGES = ["en", "es", "ru", "it"]
+LANGUAGES = [
+ "af",
+ "al",
+ "ar",
+ "az",
+ "bg",
+ "ca",
+ "cz",
+ "da",
+ "de",
+ "el",
+ "en",
+ "es",
+ "eu",
+ "fa",
+ "fi",
+ "fr",
+ "gl",
+ "he",
+ "hi",
+ "hr",
+ "hu",
+ "id",
+ "it",
+ "ja",
+ "kr",
+ "la",
+ "lt",
+ "mk",
+ "nl",
+ "no",
+ "pl",
+ "pt",
+ "pt_br",
+ "ro",
+ "ru",
+ "se",
+ "sk",
+ "sl",
+ "sp",
+ "sr",
+ "sv",
+ "th",
+ "tr",
+ "ua",
+ "uk",
+ "vi",
+ "zh_cn",
+ "zh_tw",
+ "zu",
+]
CONDITION_CLASSES = {
"cloudy": [803, 804],
"fog": [701, 741],
@@ -112,8 +163,8 @@
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
},
ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE},
- ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: "mm"},
- ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: "mm"},
+ ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: LENGTH_MILLIMETERS},
+ ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: LENGTH_MILLIMETERS},
ATTR_API_CONDITION: {SENSOR_NAME: "Condition"},
ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"},
}
diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json
index e068bb919642b1..15b5c0f4d57587 100644
--- a/homeassistant/components/openweathermap/strings.json
+++ b/homeassistant/components/openweathermap/strings.json
@@ -1,19 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "OpenWeatherMap integration for these coordinates is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%] for these coordinates."
},
"error": {
- "auth": "API key is not correct.",
- "connection": "Can't connect to OWM API"
+ "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": "OpenWeatherMap API key",
+ "api_key": "[%key:common::config_flow::data::api_key%]",
"language": "Language",
- "latitude": "Latitude",
- "longitude": "Longitude",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
"mode": "Mode",
"name": "Name of the integration"
},
diff --git a/homeassistant/components/openweathermap/translations/ca.json b/homeassistant/components/openweathermap/translations/ca.json
index 240e378c0fa459..46b721f86cb5fc 100644
--- a/homeassistant/components/openweathermap/translations/ca.json
+++ b/homeassistant/components/openweathermap/translations/ca.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "La integraci\u00f3 OpenWeatherMap per a aquestes coordenades ja est\u00e0 configurada."
+ "already_configured": "El servei ja est\u00e0 configurat per a aquestes coordenades."
},
"error": {
"auth": "La clau API no \u00e9s correcta.",
- "connection": "No s'ha pogut connectar amb l'API d'OWM"
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "connection": "No s'ha pogut connectar amb l'API d'OWM",
+ "invalid_api_key": "Clau API inv\u00e0lida"
},
"step": {
"user": {
"data": {
- "api_key": "Clau API d'OpenWeatherMap",
+ "api_key": "Clau API",
"language": "Idioma",
"latitude": "Latitud",
"longitude": "Longitud",
diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json
new file mode 100644
index 00000000000000..6582b2046b81c3
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/de.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "error": {
+ "auth": "Der API-Schl\u00fcssel ist nicht korrekt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "language": "Sprache",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "mode": "Modus"
+ },
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Sprache",
+ "mode": "Modus"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json
new file mode 100644
index 00000000000000..bdd4c34a58609d
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/el.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenWeatherMap \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af."
+ },
+ "error": {
+ "auth": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc.",
+ "connection": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf OWM API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API OpenWeatherMap",
+ "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1",
+ "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2",
+ "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2",
+ "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1",
+ "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2"
+ },
+ "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenWeatherMap. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1",
+ "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/en.json b/homeassistant/components/openweathermap/translations/en.json
index d42c4479711fb8..1689f7e201b169 100644
--- a/homeassistant/components/openweathermap/translations/en.json
+++ b/homeassistant/components/openweathermap/translations/en.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "OpenWeatherMap integration for these coordinates is already configured."
+ "already_configured": "Service is already configured for these coordinates."
},
"error": {
"auth": "API key is not correct.",
- "connection": "Can't connect to OWM API"
+ "cannot_connect": "Failed to connect",
+ "connection": "Can't connect to OWM API",
+ "invalid_api_key": "Invalid API key"
},
"step": {
"user": {
"data": {
- "api_key": "OpenWeatherMap API key",
+ "api_key": "API Key",
"language": "Language",
"latitude": "Latitude",
"longitude": "Longitude",
diff --git a/homeassistant/components/openweathermap/translations/es.json b/homeassistant/components/openweathermap/translations/es.json
new file mode 100644
index 00000000000000..a3d783ef84dceb
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/es.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La integraci\u00f3n de OpenWeatherMap para estas coordenadas ya est\u00e1 configurada."
+ },
+ "error": {
+ "auth": "La clave de API no es correcta.",
+ "cannot_connect": "No se pudo conectar",
+ "connection": "No se puede conectar a la API de OWM",
+ "invalid_api_key": "Clave API no v\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clave de API de OpenWeatherMap",
+ "language": "Idioma",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "mode": "Modo",
+ "name": "Nombre de la integraci\u00f3n"
+ },
+ "description": "Configurar la integraci\u00f3n de OpenWeatherMap. Para generar la clave API, ve a https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Idioma",
+ "mode": "Modo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json
new file mode 100644
index 00000000000000..6ae485f2350778
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/et.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Nende koordinaatidele on OpenWeatherMapi sidumine juba tehtud."
+ },
+ "error": {
+ "auth": "API v\u00f5ti on vale.",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection": "OWM API-ga ei saa \u00fchendust luua",
+ "invalid_api_key": "Vigane API v\u00f5ti"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMapi API v\u00f5ti",
+ "language": "Keel",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "mode": "Re\u017eiim",
+ "name": "Sidumise nimi"
+ },
+ "description": "Seadistage OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks minge aadressile https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Keel",
+ "mode": "Re\u017eiim"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json
index ab53d663f48add..aaa7887c120f81 100644
--- a/homeassistant/components/openweathermap/translations/fr.json
+++ b/homeassistant/components/openweathermap/translations/fr.json
@@ -5,7 +5,9 @@
},
"error": {
"auth": "La cl\u00e9 API n'est pas correcte.",
- "connection": "Impossible de se connecter \u00e0 l'API OWM"
+ "cannot_connect": "\u00c9chec de connexion",
+ "connection": "Impossible de se connecter \u00e0 l'API OWM",
+ "invalid_api_key": "Cl\u00e9 API invalide"
},
"step": {
"user": {
diff --git a/homeassistant/components/openweathermap/translations/it.json b/homeassistant/components/openweathermap/translations/it.json
index c53e88d9558b1e..7f4dc75e1829eb 100644
--- a/homeassistant/components/openweathermap/translations/it.json
+++ b/homeassistant/components/openweathermap/translations/it.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "L'integrazione di OpenWeatherMap per queste coordinate \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato per queste coordinate."
},
"error": {
"auth": "La chiave API non \u00e8 corretta.",
- "connection": "Impossibile connettersi all'API OWM"
+ "cannot_connect": "Impossibile connettersi",
+ "connection": "Impossibile connettersi all'API OWM",
+ "invalid_api_key": "Chiave API non valida"
},
"step": {
"user": {
"data": {
- "api_key": "Chiave API OpenWeatherMap",
+ "api_key": "Chiave API",
"language": "Lingua",
"latitude": "Latitudine",
"longitude": "Logitudine",
diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json
new file mode 100644
index 00000000000000..12e76d85506c67
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/ko.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc774\ub7ec\ud55c \uc88c\ud45c\uc5d0 \ub300\ud55c OpenWeatherMap \ud1b5\ud569\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
+ "connection": "OWM API\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API \ud0a4",
+ "language": "\uc5b8\uc5b4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "mode": "\ubaa8\ub4dc",
+ "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uba85"
+ },
+ "description": "OpenWeatherMap \ud1b5\ud569\uc744 \uc124\uc815\ud558\uc138\uc694. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid\ub85c \uc774\ub3d9\ud558\uc2ed\uc2dc\uc624.",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "\uc5b8\uc5b4",
+ "mode": "\ubaa8\ub4dc"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/lb.json b/homeassistant/components/openweathermap/translations/lb.json
new file mode 100644
index 00000000000000..ffc3a75a4530f8
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/lb.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "error": {
+ "auth": "Api Schl\u00ebssel ass net korrekt.",
+ "connection": "Kann sech net mat der OWM API verbannen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API Schl\u00ebssel",
+ "language": "Sproch",
+ "latitude": "Breedegrad",
+ "longitude": "L\u00e4ngegrad",
+ "mode": "Modus",
+ "name": "Numm vun der Integratioun"
+ },
+ "description": "OpenWeatherMap Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle g\u00e9i op https://openweathermap.org/appid",
+ "title": "OpenWeatherMap API Schl\u00ebssel"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Sproch",
+ "mode": "Modus"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json
new file mode 100644
index 00000000000000..fdff089bddb148
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/nl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "OpenWeatherMap-integratie voor deze co\u00f6rdinaten is al geconfigureerd."
+ },
+ "error": {
+ "auth": "API-sleutel is niet correct.",
+ "connection": "Kan geen verbinding maken met OWM API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API-sleutel",
+ "language": "Taal",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "mode": "Mode",
+ "name": "Naam van de integratie"
+ },
+ "description": "Stel OpenWeatherMap-integratie in. Ga naar https://openweathermap.org/appid om een API-sleutel te genereren",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Taal",
+ "mode": "Mode"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/no.json b/homeassistant/components/openweathermap/translations/no.json
index cda3666ff18ab5..c83d60d7856073 100644
--- a/homeassistant/components/openweathermap/translations/no.json
+++ b/homeassistant/components/openweathermap/translations/no.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "OpenWeatherMap-integrasjon for disse koordinatene er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert for these coordinates."
},
"error": {
"auth": "API-n\u00f8kkelen er ikke korrekt.",
- "connection": "Kan ikke koble til OWM API"
+ "cannot_connect": "Tilkobling mislyktes.",
+ "connection": "Kan ikke koble til OWM API",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel"
},
"step": {
"user": {
"data": {
- "api_key": "OpenWeatherMap API-n\u00f8kkel",
+ "api_key": "API-n\u00f8kkel",
"language": "Spr\u00e5k",
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
diff --git a/homeassistant/components/openweathermap/translations/pl.json b/homeassistant/components/openweathermap/translations/pl.json
index 74e74422bd2140..153228485edfa1 100644
--- a/homeassistant/components/openweathermap/translations/pl.json
+++ b/homeassistant/components/openweathermap/translations/pl.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana. dla tych wsp\u00f3\u0142rz\u0119dnych."
+ },
"error": {
"auth": "Klucz API jest nieprawid\u0142owy.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z OWM"
},
"step": {
@@ -14,6 +18,7 @@
"mode": "Tryb",
"name": "Nazwa integracji"
},
+ "description": "Konfiguracja integracji OpenWeatherMap. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://openweathermap.org/appid",
"title": "OpenWeatherMap"
}
}
diff --git a/homeassistant/components/openweathermap/translations/ru.json b/homeassistant/components/openweathermap/translations/ru.json
index a79e51f6053922..1721102bf74bf8 100644
--- a/homeassistant/components/openweathermap/translations/ru.json
+++ b/homeassistant/components/openweathermap/translations/ru.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenWeatherMap \u0441 \u0442\u0430\u043a\u0438\u043c\u0438 \u0436\u0435 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430."
+ "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."
},
"error": {
"auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
- "connection": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API OpenWeatherMap."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "connection": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API OpenWeatherMap.",
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
},
"step": {
"user": {
diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json
new file mode 100644
index 00000000000000..a6fe05a8346560
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/sv.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad."
+ },
+ "error": {
+ "auth": "API-nyckeln \u00e4r inte korrekt.",
+ "connection": "Kan inte ansluta till OWM API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API-nyckel",
+ "language": "Spr\u00e5k",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "mode": "L\u00e4ge",
+ "name": "Integrationens namn"
+ },
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Spr\u00e5k",
+ "mode": "L\u00e4ge"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/zh-Hant.json b/homeassistant/components/openweathermap/translations/zh-Hant.json
index e1ed3c9f89d7bb..e3b6591734713f 100644
--- a/homeassistant/components/openweathermap/translations/zh-Hant.json
+++ b/homeassistant/components/openweathermap/translations/zh-Hant.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\u6b64 OpenWeatherMap \u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u6b64\u4e9b\u5354\u8abf\u5668\u4e4b\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
},
"error": {
"auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002",
- "connection": "\u7121\u6cd5\u9023\u7dda\u81f3 OWM API"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "connection": "\u7121\u6cd5\u9023\u7dda\u81f3 OWM API",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
},
"step": {
"user": {
"data": {
- "api_key": "OpenWeatherMap API \u5bc6\u9470",
+ "api_key": "API \u5bc6\u9470",
"language": "\u8a9e\u8a00",
"latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6",
diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py
index c64e0b0679a759..f7743d04a6d9f0 100644
--- a/homeassistant/components/opnsense/device_tracker.py
+++ b/homeassistant/components/opnsense/device_tracker.py
@@ -61,6 +61,6 @@ def get_extra_attributes(self, device):
if device not in self.last_results:
return None
mfg = self.last_results[device].get("manufacturer")
- if mfg:
- return {"manufacturer": mfg}
- return {}
+ if not mfg:
+ return {}
+ return {"manufacturer": mfg}
diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py
index fec30cdade7b8d..a03d9fc5ff0d8a 100644
--- a/homeassistant/components/orvibo/switch.py
+++ b/homeassistant/components/orvibo/switch.py
@@ -73,11 +73,6 @@ def __init__(self, name, s20):
self._state = False
self._exc = S20Exception
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def name(self):
"""Return the name of the switch."""
diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json
new file mode 100644
index 00000000000000..6f39806287630f
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json
index f8bdce3d3a20da..b900e75787f3b3 100644
--- a/homeassistant/components/ovo_energy/translations/fr.json
+++ b/homeassistant/components/ovo_energy/translations/fr.json
@@ -11,6 +11,7 @@
"password": "Mot de passe",
"username": "Nom d'utilisateur"
},
+ "description": "Configurez une instance OVO Energy pour acc\u00e9der \u00e0 votre consommation d'\u00e9nergie.",
"title": "Ajouter un compte OVO Energy"
}
}
diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json
new file mode 100644
index 00000000000000..f5481afa94af8e
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json
index 09d002bc161098..7d6882c2bd3dba 100644
--- a/homeassistant/components/ovo_energy/translations/ko.json
+++ b/homeassistant/components/ovo_energy/translations/ko.json
@@ -1,7 +1,9 @@
{
"config": {
"error": {
- "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "authorization_error": "\uc778\uc99d \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uaca9 \uc99d\uba85\uc744 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.",
+ "connection_error": "\uc5f0\uacb0 \uc2e4\ud328"
},
"step": {
"user": {
diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json
index 42afe86d48a0f7..092d65aef8ea7d 100644
--- a/homeassistant/components/ovo_energy/translations/pl.json
+++ b/homeassistant/components/ovo_energy/translations/pl.json
@@ -1,7 +1,19 @@
{
"config": {
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "authorization_error": "B\u0142\u0105d autoryzacji. Sprawd\u017a swoje po\u015bwiadczenia.",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Skonfiguruj instancj\u0119 OVO Energy, aby uzyska\u0107 dost\u0119p do swojego zu\u017cycia energii.",
+ "title": "Dodaj konto OVO Energy"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py
index 24dc99de71ce3e..d3091d7d027135 100644
--- a/homeassistant/components/owntracks/__init__.py
+++ b/homeassistant/components/owntracks/__init__.py
@@ -9,7 +9,12 @@
from homeassistant import config_entries
from homeassistant.components import mqtt
-from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.const import (
+ ATTR_GPS_ACCURACY,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ CONF_WEBHOOK_ID,
+)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.setup import async_when_setup
@@ -292,9 +297,9 @@ def async_see_beacons(self, hass, dev_id, kwargs_param):
device_tracker_state = hass.states.get(f"device_tracker.{dev_id}")
if device_tracker_state is not None:
- acc = device_tracker_state.attributes.get("gps_accuracy")
- lat = device_tracker_state.attributes.get("latitude")
- lon = device_tracker_state.attributes.get("longitude")
+ acc = device_tracker_state.attributes.get(ATTR_GPS_ACCURACY)
+ lat = device_tracker_state.attributes.get(ATTR_LATITUDE)
+ lon = device_tracker_state.attributes.get(ATTR_LONGITUDE)
if lat is not None and lon is not None:
kwargs["gps"] = (lat, lon)
diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py
index 0aba24217cc6fe..f0838b510ec8b3 100644
--- a/homeassistant/components/owntracks/config_flow.py
+++ b/homeassistant/components/owntracks/config_flow.py
@@ -19,7 +19,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle a user initiated set up flow to create OwnTracks webhook."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user")
@@ -52,7 +52,7 @@ async def async_step_user(self, user_input=None):
async def async_step_import(self, user_input):
"""Import a config flow from configuration."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
webhook_id, _webhook_url, cloudhook = await self._get_webhook_id()
secret = secrets.token_hex(16)
return self.async_create_entry(
diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py
index 5e610d861fe1cf..3a4aac6bfd1bde 100644
--- a/homeassistant/components/owntracks/messages.py
+++ b/homeassistant/components/owntracks/messages.py
@@ -10,7 +10,7 @@
SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_GPS,
)
-from homeassistant.const import STATE_HOME
+from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME
from homeassistant.util import decorator, slugify
from .helper import supports_encryption
@@ -97,7 +97,10 @@ def _set_gps_from_zone(kwargs, location, zone):
Async friendly.
"""
if zone is not None:
- kwargs["gps"] = (zone.attributes["latitude"], zone.attributes["longitude"])
+ kwargs["gps"] = (
+ zone.attributes[ATTR_LATITUDE],
+ zone.attributes[ATTR_LONGITUDE],
+ )
kwargs["gps_accuracy"] = zone.attributes["radius"]
kwargs["location_name"] = location
return kwargs
diff --git a/homeassistant/components/owntracks/strings.json b/homeassistant/components/owntracks/strings.json
index 12aba21be72ce3..ddb700cc6424bb 100644
--- a/homeassistant/components/owntracks/strings.json
+++ b/homeassistant/components/owntracks/strings.json
@@ -6,7 +6,9 @@
"description": "Are you sure you want to set up OwnTracks?"
}
},
- "abort": { "one_instance_allowed": "Only a single instance is necessary." },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
"create_entry": {
"default": "\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information."
}
diff --git a/homeassistant/components/owntracks/translations/ca.json b/homeassistant/components/owntracks/translations/ca.json
index 6979bcb31a827c..c7cf0bb457a2ca 100644
--- a/homeassistant/components/owntracks/translations/ca.json
+++ b/homeassistant/components/owntracks/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "\n\nPer Android: obre [l'app d'OwnTracks]({android_url}), ves a prefer\u00e8ncies -> connexi\u00f3. Canvia els par\u00e0metres seguents:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nPer iOS: obre [l'app d'OwnTracks]({ios_url}), clica l'icona (i) a dalt a l'esquerra -> configuraci\u00f3 (settings). Canvia els par\u00e0metres seg\u00fcents:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nConsulta [la documentaci\u00f3]({docs_url}) per a m\u00e9s informaci\u00f3."
diff --git a/homeassistant/components/owntracks/translations/el.json b/homeassistant/components/owntracks/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/owntracks/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/translations/en.json b/homeassistant/components/owntracks/translations/en.json
index a1cf38a91a2429..9090cea70f2c8e 100644
--- a/homeassistant/components/owntracks/translations/en.json
+++ b/homeassistant/components/owntracks/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information."
diff --git a/homeassistant/components/owntracks/translations/es.json b/homeassistant/components/owntracks/translations/es.json
index ce558a70cdd280..0bc37535fb3574 100644
--- a/homeassistant/components/owntracks/translations/es.json
+++ b/homeassistant/components/owntracks/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "\n\nEn Android, abre [la aplicaci\u00f3n OwnTracks]({android_url}), ve a preferencias -> conexi\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP privado\n - Host: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abre [la aplicaci\u00f3n OwnTracks] ({ios_url}), pulsa el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n."
diff --git a/homeassistant/components/owntracks/translations/et.json b/homeassistant/components/owntracks/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/owntracks/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json
index 69f36504051cdb..ba894ff43e76f0 100644
--- a/homeassistant/components/owntracks/translations/fr.json
+++ b/homeassistant/components/owntracks/translations/fr.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "\n\n Sous 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."
diff --git a/homeassistant/components/owntracks/translations/it.json b/homeassistant/components/owntracks/translations/it.json
index a198bc33fda34e..60485d590b2405 100644
--- a/homeassistant/components/owntracks/translations/it.json
+++ b/homeassistant/components/owntracks/translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "\n\nSu Android, apri l'[app OwnTracks]({android_url}), vai su preferenze -> connessione. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP privato \n - Host: {webhook_url} \n - Identificazione: \n - Nome utente: `''` \n - ID dispositivo: `''`\n\nSu iOS, apri l'[app OwnTracks]({ios_url}), tocca l'icona (i) in alto a sinistra -> impostazioni. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP \n - URL: {webhook_url} \n - Attiva autenticazione \n - UserID: `''` \n\n {secret} \n \n Vedi [la documentazione]({docs_url}) per maggiori informazioni."
diff --git a/homeassistant/components/owntracks/translations/lb.json b/homeassistant/components/owntracks/translations/lb.json
index 4504ca6a74a7f0..4c9cba9e6ca0dd 100644
--- a/homeassistant/components/owntracks/translations/lb.json
+++ b/homeassistant/components/owntracks/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "\n\nOp Android, an [der OwnTracks App]({android_url}), g\u00e9i an Preferences -> Connection. \u00c4nnert folgend Astellungen:\n- Mode: Private HTTP\n- Host {webhool_url}\n- Identification:\n - Username: ``\n - Device ID: ``\n\nOp IOS, an [der OwnTracks App]({ios_url}), klick op (i) Ikon uewen l\u00e9nks -> Settings. \u00c4nnert folgend Astellungen:\n- Mode: HTTP\n- URL: {webhool_url}\n- Turn on authentication:\n- UserID: ``\n\n{secret}\n\nKuckt w.e.g. [Dokumentatioun]({docs_url}) fir m\u00e9i Informatiounen."
diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json
index 594f6f923cbfac..1ff911561647fc 100644
--- a/homeassistant/components/owntracks/translations/no.json
+++ b/homeassistant/components/owntracks/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identificasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autensiering\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon."
diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json
index 816f2c8608744d..c04394820f975a 100644
--- a/homeassistant/components/owntracks/translations/ru.json
+++ b/homeassistant/components/owntracks/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\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]({docs_url}) \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."
diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json
index d86a5376d077a3..4d9871ffae23c3 100644
--- a/homeassistant/components/owntracks/translations/zh-Hant.json
+++ b/homeassistant/components/owntracks/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py
index 8822490132c54c..3153324322e034 100644
--- a/homeassistant/components/ozw/config_flow.py
+++ b/homeassistant/components/ozw/config_flow.py
@@ -15,7 +15,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
if "mqtt" not in self.hass.config.components:
return self.async_abort(reason="mqtt_required")
if user_input is not None:
diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py
index 5164bbbe0445f0..8e5007a8419d0e 100644
--- a/homeassistant/components/ozw/const.py
+++ b/homeassistant/components/ozw/const.py
@@ -40,6 +40,7 @@
# 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
diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py
index 8bbb6741020c6e..a83f763c8106cd 100644
--- a/homeassistant/components/ozw/discovery.py
+++ b/homeassistant/components/ozw/discovery.py
@@ -136,6 +136,7 @@
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: {
diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py
index 3797734dfeb28d..68acb3f96913af 100644
--- a/homeassistant/components/ozw/lock.py
+++ b/homeassistant/components/ozw/lock.py
@@ -1,7 +1,9 @@
"""Representation of Z-Wave locks."""
import logging
-from openzwavemqtt.const import CommandClass, ValueIndex
+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
@@ -12,7 +14,6 @@
from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity
-ATTR_CODE_SLOT = "code_slot"
ATTR_USERCODE = "usercode"
SERVICE_SET_USERCODE = "set_usercode"
@@ -54,6 +55,17 @@ def async_add_lock(value):
)
+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."""
@@ -73,25 +85,15 @@ async def async_unlock(self, **kwargs):
@callback
def async_set_usercode(self, code_slot, usercode):
"""Set the usercode to index X on the lock."""
- value = self.values.primary.node.get_value(CommandClass.USER_CODE, code_slot)
-
- if len(str(usercode)) < 4:
- _LOGGER.error(
- "Invalid code provided: (%s) user code must be at least 4 digits",
- usercode,
- )
- return
- value.send_value(usercode)
- _LOGGER.debug("User code at slot %s set", code_slot)
+ 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."""
- value = self.values.primary.node.get_value(
- CommandClass.USER_CODE, ValueIndex.CLEAR_USER_CODE
- )
-
- value.send_value(code_slot)
- # Sending twice because the first time it doesn't take
- value.send_value(code_slot)
- _LOGGER.info("Usercode at slot %s is cleared", code_slot)
+ 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
index 6fa4174da4aa4e..667a50428e19e4 100644
--- a/homeassistant/components/ozw/manifest.json
+++ b/homeassistant/components/ozw/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ozw",
"requirements": [
- "python-openzwave-mqtt==1.0.5"
+ "python-openzwave-mqtt==1.2.0"
],
"after_dependencies": [
"mqtt"
diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py
index 500289cec21508..e1f71e636b3efd 100644
--- a/homeassistant/components/ozw/services.py
+++ b/homeassistant/components/ozw/services.py
@@ -1,7 +1,8 @@
"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""
import logging
-from openzwavemqtt.const import CommandClass, ValueType
+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 callback
@@ -42,6 +43,14 @@ def async_register(self):
{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,
@@ -53,7 +62,24 @@ def async_register(self):
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.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,
),
}
),
@@ -66,57 +92,12 @@ def async_set_config_parameter(self, service):
node_id = service.data[const.ATTR_NODE_ID]
param = service.data[const.ATTR_CONFIG_PARAMETER]
selection = service.data[const.ATTR_CONFIG_VALUE]
- payload = None
-
- value = (
- self._manager.get_instance(instance_id)
- .get_node(node_id)
- .get_value(CommandClass.CONFIGURATION, param)
- )
-
- if value.type == ValueType.BOOL:
- payload = selection == "True"
- if value.type == ValueType.LIST:
- # accept either string from the list value OR the int value
- for selected in value.value["List"]:
- if selection not in (selected["Label"], selected["Value"]):
- continue
- payload = int(selected["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)
- if payload is None:
- _LOGGER.error(
- "Invalid value %s for parameter %s",
- selection,
- param,
- )
- return
-
- if value.type == ValueType.BUTTON:
- # Unsupported at this time
- _LOGGER.info("Button type not supported yet")
- return
-
- if value.type == ValueType.STRING:
- payload = selection
-
- if (
- value.type == ValueType.INT
- or value.type == ValueType.BYTE
- or value.type == ValueType.SHORT
- ):
- if selection > value.max or selection < value.min:
- _LOGGER.error(
- "Value %s out of range for parameter %s (Min: %s Max: %s)",
- selection,
- param,
- value.min,
- value.max,
- )
- return
- payload = int(selection)
-
- value.send_value(payload) # send the payload
_LOGGER.info(
"Setting configuration parameter %s on Node %s with value %s",
param,
@@ -130,6 +111,8 @@ def async_add_node(self, service):
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
@@ -137,4 +120,15 @@ def async_remove_node(self, service):
"""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):
+ """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
index d7f5c540c67a27..641c086f524465 100644
--- a/homeassistant/components/ozw/services.yaml
+++ b/homeassistant/components/ozw/services.yaml
@@ -13,6 +13,12 @@ remove_node:
instance_id:
description: (Optional) The OZW Instance/Controller to use, defaults to 1.
+cancel_command:
+ description: Cancel a pending add or remove node command.
+ fields:
+ instance_id:
+ description: (Optional) The OZW Instance/Controller to use, defaults to 1.
+
set_config_parameter:
description: Set a config parameter to a node on the Z-Wave network.
fields:
diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json
index dd2aad7e4ce078..88f8911db0d5bc 100644
--- a/homeassistant/components/ozw/strings.json
+++ b/homeassistant/components/ozw/strings.json
@@ -6,7 +6,7 @@
}
},
"abort": {
- "one_instance_allowed": "The integration only supports one Z-Wave instance",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"mqtt_required": "The MQTT integration is not set up"
}
}
diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json
index eba9a7e87572ae..3a1fdb29ba14f7 100644
--- a/homeassistant/components/ozw/translations/ca.json
+++ b/homeassistant/components/ozw/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada",
- "one_instance_allowed": "La integraci\u00f3 nom\u00e9s admet una inst\u00e0ncia Z-Wave"
+ "one_instance_allowed": "La integraci\u00f3 nom\u00e9s admet una inst\u00e0ncia Z-Wave",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/el.json b/homeassistant/components/ozw/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/ozw/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json
index c6a45474880c86..4e41ee58d11b0d 100644
--- a/homeassistant/components/ozw/translations/en.json
+++ b/homeassistant/components/ozw/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "The MQTT integration is not set up",
- "one_instance_allowed": "The integration only supports one Z-Wave instance"
+ "one_instance_allowed": "The integration only supports one Z-Wave instance",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json
index f78b62828cb9f6..66cebc62ec4fd9 100644
--- a/homeassistant/components/ozw/translations/es.json
+++ b/homeassistant/components/ozw/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada",
- "one_instance_allowed": "La integraci\u00f3n solo admite una instancia de Z-Wave"
+ "one_instance_allowed": "La integraci\u00f3n solo admite una instancia de Z-Wave",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json
new file mode 100644
index 00000000000000..16196205aec54e
--- /dev/null
+++ b/homeassistant/components/ozw/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "mqtt_required": "MQTT sidumine pole seadistatud",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json
index dbc609b93eb365..0c6c1e82da5551 100644
--- a/homeassistant/components/ozw/translations/fr.json
+++ b/homeassistant/components/ozw/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e",
- "one_instance_allowed": "L'int\u00e9gration ne prend en charge qu'une seule instance Z-Wave"
+ "one_instance_allowed": "L'int\u00e9gration ne prend en charge qu'une seule instance Z-Wave",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json
index 0b76d09cf08439..f97be82b374bf8 100644
--- a/homeassistant/components/ozw/translations/it.json
+++ b/homeassistant/components/ozw/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "L'integrazione MQTT non \u00e8 impostata",
- "one_instance_allowed": "L'integrazione supporta solo un'istanza Z-Wave"
+ "one_instance_allowed": "L'integrazione supporta solo un'istanza Z-Wave",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json
index 053fe63113368b..9b2a5e577c2ce4 100644
--- a/homeassistant/components/ozw/translations/lb.json
+++ b/homeassistant/components/ozw/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "MQTT Integratioun ass net ageriicht",
- "one_instance_allowed": "D'Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen 1 Z-Wave Instanz"
+ "one_instance_allowed": "D'Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen 1 Z-Wave Instanz",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json
index 1d4049978f5a9f..62448d2a43eeb2 100644
--- a/homeassistant/components/ozw/translations/no.json
+++ b/homeassistant/components/ozw/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "MQTT-integrasjonen er ikke satt opp",
- "one_instance_allowed": "Integrasjonen st\u00f8tter bare \u00e9n Z-Wave-forekomst"
+ "one_instance_allowed": "Integrasjonen st\u00f8tter bare \u00e9n Z-Wave-forekomst",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json
index ac968e9fdfa41b..71f21f9eb1709c 100644
--- a/homeassistant/components/ozw/translations/ru.json
+++ b/homeassistant/components/ozw/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.",
- "one_instance_allowed": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 Z-Wave."
+ "one_instance_allowed": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 Z-Wave.",
+ "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": {
diff --git a/homeassistant/components/ozw/translations/zh-Hans.json b/homeassistant/components/ozw/translations/zh-Hans.json
index e4beac109fdf19..a3b0a71d63c1dc 100644
--- a/homeassistant/components/ozw/translations/zh-Hans.json
+++ b/homeassistant/components/ozw/translations/zh-Hans.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "mqtt_required": "\u672a\u8bbe\u7f6e MQTT \u96c6\u6210"
+ "mqtt_required": "\u672a\u8bbe\u7f6e MQTT \u96c6\u6210",
+ "one_instance_allowed": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a Z-Wave \u5b9e\u4f8b"
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json
index e9ad87042d2661..d9967bddae126c 100644
--- a/homeassistant/components/ozw/translations/zh-Hant.json
+++ b/homeassistant/components/ozw/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a",
- "one_instance_allowed": "\u6574\u5408\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u5be6\u4f8b"
+ "one_instance_allowed": "\u6574\u5408\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u5be6\u4f8b",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"user": {
diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py
index da9c2b1f598051..482d78bb8784b4 100644
--- a/homeassistant/components/ozw/websocket_api.py
+++ b/homeassistant/components/ozw/websocket_api.py
@@ -1,21 +1,34 @@
"""Web socket API for OpenZWave."""
-
-import logging
-
-from openzwavemqtt.const import EVENT_NODE_ADDED, EVENT_NODE_CHANGED
+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
from homeassistant.components import websocket_api
from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv
-from .const import DOMAIN, MANAGER, OPTIONS
-
-_LOGGER = logging.getLogger(__name__)
+from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER, OPTIONS
+from .lock import ATTR_USERCODE
TYPE = "type"
ID = "id"
OZW_INSTANCE = "ozw_instance"
NODE_ID = "node_id"
+PARAMETER = ATTR_CONFIG_PARAMETER
+VALUE = ATTR_CONFIG_VALUE
ATTR_NODE_QUERY_STAGE = "node_query_stage"
ATTR_IS_ZWAVE_PLUS = "is_zwave_plus"
@@ -45,6 +58,52 @@ def async_register_api(hass):
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])
@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
@@ -102,6 +161,94 @@ def websocket_get_nodes(hass, connection, msg):
)
+@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_parameters)
+
+
+@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, False, set_config_parameter, msg[PARAMETER], msg[VALUE]
+ )
+
+
@websocket_api.websocket_command(
{
vol.Required(TYPE): "ozw/network_status",
@@ -148,14 +295,15 @@ def websocket_network_statistics(hass, connection, msg):
)
def websocket_node_status(hass, connection, msg):
"""Get the status for a Z-Wave node."""
- manager = hass.data[DOMAIN][MANAGER]
- node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID])
-
- if not node:
- connection.send_message(
- websocket_api.error_message(
- msg[ID], websocket_api.const.ERR_NOT_FOUND, "OZW Node not found"
- )
+ 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
@@ -192,14 +340,15 @@ def websocket_node_status(hass, connection, msg):
)
def websocket_node_metadata(hass, connection, msg):
"""Get the metadata for a Z-Wave node."""
- manager = hass.data[DOMAIN][MANAGER]
- node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID])
-
- if not node:
- connection.send_message(
- websocket_api.error_message(
- msg[ID], websocket_api.const.ERR_NOT_FOUND, "OZW Node not found"
- )
+ 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
diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py
index d08e293eb451f7..8489c69fe66c87 100644
--- a/homeassistant/components/panasonic_viera/config_flow.py
+++ b/homeassistant/components/panasonic_viera/config_flow.py
@@ -17,9 +17,6 @@
DEFAULT_PORT,
DOMAIN,
ERROR_INVALID_PIN_CODE,
- ERROR_NOT_CONNECTED,
- REASON_NOT_CONNECTED,
- REASON_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__)
@@ -54,10 +51,10 @@ async def async_step_user(self, user_input=None):
)
except (TimeoutError, URLError, SOAPError, OSError) as err:
_LOGGER.error("Could not establish remote connection: %s", err)
- errors["base"] = ERROR_NOT_CONNECTED
+ errors["base"] = "cannot_connect"
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("An unknown error occurred: %s", err)
- return self.async_abort(reason=REASON_UNKNOWN)
+ return self.async_abort(reason="unknown")
if "base" not in errors:
if self._remote.type == TV_TYPE_ENCRYPTED:
@@ -104,10 +101,10 @@ async def async_step_pairing(self, user_input=None):
errors["base"] = ERROR_INVALID_PIN_CODE
except (TimeoutError, URLError, OSError) as err:
_LOGGER.error("The remote connection was lost: %s", err)
- return self.async_abort(reason=REASON_NOT_CONNECTED)
+ return self.async_abort(reason="cannot_connect")
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unknown error: %s", err)
- return self.async_abort(reason=REASON_UNKNOWN)
+ return self.async_abort(reason="unknown")
if "base" not in errors:
encryption_data = {
@@ -128,10 +125,10 @@ async def async_step_pairing(self, user_input=None):
)
except (TimeoutError, URLError, SOAPError, OSError) as err:
_LOGGER.error("The remote connection was lost: %s", err)
- return self.async_abort(reason=REASON_NOT_CONNECTED)
+ return self.async_abort(reason="cannot_connect")
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unknown error: %s", err)
- return self.async_abort(reason=REASON_UNKNOWN)
+ return self.async_abort(reason="unknown")
return self.async_show_form(
step_id="pairing",
diff --git a/homeassistant/components/panasonic_viera/const.py b/homeassistant/components/panasonic_viera/const.py
index 529de4ebe672cc..2fb264a6b1c51c 100644
--- a/homeassistant/components/panasonic_viera/const.py
+++ b/homeassistant/components/panasonic_viera/const.py
@@ -12,8 +12,4 @@
ATTR_REMOTE = "remote"
-ERROR_NOT_CONNECTED = "not_connected"
ERROR_INVALID_PIN_CODE = "invalid_pin_code"
-
-REASON_NOT_CONNECTED = "not_connected"
-REASON_UNKNOWN = "unknown"
diff --git a/homeassistant/components/panasonic_viera/strings.json b/homeassistant/components/panasonic_viera/strings.json
index f3943fde71d30c..fdb9af28303df8 100644
--- a/homeassistant/components/panasonic_viera/strings.json
+++ b/homeassistant/components/panasonic_viera/strings.json
@@ -7,25 +7,25 @@
"description": "Enter your Panasonic Viera TV's IP address",
"data": {
"host": "[%key:common::config_flow::data::ip%]",
- "name": "Name"
+ "name": "[%key:common::config_flow::data::name%]"
}
},
"pairing": {
"title": "Pairing",
"description": "Enter the PIN displayed on your TV",
"data": {
- "pin": "PIN"
+ "pin": "[%key:common::config_flow::data::pin%]"
}
}
},
"error": {
- "not_connected": "Could not establish a remote connection with your Panasonic Viera TV",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_pin_code": "The PIN code you entered was invalid"
},
"abort": {
- "already_configured": "This Panasonic Viera TV is already configured.",
- "not_connected": "The remote connection with your Panasonic Viera TV was lost. Check the logs for more information.",
- "unknown": "An unknown error occured. Check the logs for more information."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}
diff --git a/homeassistant/components/panasonic_viera/translations/fr.json b/homeassistant/components/panasonic_viera/translations/fr.json
index 4ee07e94ad4563..9f8c9b672e526d 100644
--- a/homeassistant/components/panasonic_viera/translations/fr.json
+++ b/homeassistant/components/panasonic_viera/translations/fr.json
@@ -1,8 +1,14 @@
{
"config": {
"abort": {
+ "already_configured": "Ce t\u00e9l\u00e9viseur Panasonic Viera est d\u00e9j\u00e0 configur\u00e9.",
+ "not_connected": "La connexion \u00e0 distance avec votre t\u00e9l\u00e9viseur Panasonic Viera a \u00e9t\u00e9 perdue. Consultez les journaux pour plus d'informations.",
"unknown": "Une erreur inconnue est survenue. Veuillez consulter les journaux pour obtenir plus de d\u00e9tails."
},
+ "error": {
+ "invalid_pin_code": "Le code PIN que vous avez entr\u00e9 n'est pas valide",
+ "not_connected": "Impossible d'\u00e9tablir une connexion \u00e0 distance avec votre t\u00e9l\u00e9viseur Panasonic Viera"
+ },
"step": {
"pairing": {
"data": {
diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py
index 459e583c26765e..33ea72f94ffec3 100644
--- a/homeassistant/components/pandora/media_player.py
+++ b/homeassistant/components/pandora/media_player.py
@@ -88,11 +88,6 @@ def __init__(self, name):
self._media_duration = 0
self._pianobar = None
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def name(self):
"""Return the name of the media player."""
diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py
new file mode 100644
index 00000000000000..07ec2cfe985ed7
--- /dev/null
+++ b/homeassistant/components/person/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)
diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json
index 42faf5d5a46aa7..3bb5289777d5b0 100644
--- a/homeassistant/components/pi_hole/strings.json
+++ b/homeassistant/components/pi_hole/strings.json
@@ -5,11 +5,11 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"location": "Location",
"api_key": "[%key:common::config_flow::data::api_key%]",
- "ssl": "Use SSL",
- "verify_ssl": "Verify SSL certificate"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json
index 1a8b91fad6d820..37d4e890ef4a5e 100644
--- a/homeassistant/components/pi_hole/translations/ca.json
+++ b/homeassistant/components/pi_hole/translations/ca.json
@@ -14,7 +14,7 @@
"location": "Ubicaci\u00f3",
"name": "Nom",
"port": "Port",
- "ssl": "Utilitza SSL",
+ "ssl": "Utilitza un certificat SSL",
"verify_ssl": "Verifica el certificat SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json
index 98ac63514b6b2e..858e7c230ac085 100644
--- a/homeassistant/components/pi_hole/translations/en.json
+++ b/homeassistant/components/pi_hole/translations/en.json
@@ -14,7 +14,7 @@
"location": "Location",
"name": "Name",
"port": "Port",
- "ssl": "Use SSL",
+ "ssl": "Uses an SSL certificate",
"verify_ssl": "Verify SSL certificate"
}
}
diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json
index b8a155e9374f8f..34590ee77bb14d 100644
--- a/homeassistant/components/pi_hole/translations/it.json
+++ b/homeassistant/components/pi_hole/translations/it.json
@@ -14,7 +14,7 @@
"location": "Posizione",
"name": "Nome",
"port": "Porta",
- "ssl": "Utilizzare SSL",
+ "ssl": "Utilizza un certificato SSL",
"verify_ssl": "Verificare il certificato SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json
index 387b6c0d1eb0ec..f6a3b66b4dc313 100644
--- a/homeassistant/components/pi_hole/translations/no.json
+++ b/homeassistant/components/pi_hole/translations/no.json
@@ -14,7 +14,7 @@
"location": "Beliggenhet",
"name": "Navn",
"port": "",
- "ssl": "Bruk SSL",
+ "ssl": "Bruker et SSL-sertifikat",
"verify_ssl": "Verifisere SSL-sertifikat"
}
}
diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json
index b974e6c04c98ee..394a24a50502a3 100644
--- a/homeassistant/components/pi_hole/translations/pl.json
+++ b/homeassistant/components/pi_hole/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"user": {
diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json
index 6f7dd24e5e2a61..eb3cfa62c623ec 100644
--- a/homeassistant/components/pi_hole/translations/ru.json
+++ b/homeassistant/components/pi_hole/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
@@ -14,7 +14,7 @@
"location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"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/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json
index 1a75757dcc6b91..1cea5a87f4b5f1 100644
--- a/homeassistant/components/pi_hole/translations/zh-Hant.json
+++ b/homeassistant/components/pi_hole/translations/zh-Hant.json
@@ -14,7 +14,7 @@
"location": "\u5ea7\u6a19",
"name": "\u540d\u7a31",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u4f7f\u7528 SSL",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
}
}
diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py
index 12d175817d74e7..11eeb02293e118 100644
--- a/homeassistant/components/pilight/light.py
+++ b/homeassistant/components/pilight/light.py
@@ -61,7 +61,20 @@ def supported_features(self):
def turn_on(self, **kwargs):
"""Turn the switch on by calling pilight.send service with on code."""
- self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
- dimlevel = int(self._brightness / (255 / self._dimlevel_max))
+ # Update brightness only if provided as an argument.
+ # This will allow the switch to keep its previous brightness level.
+ dimlevel = None
+
+ if ATTR_BRIGHTNESS in kwargs:
+ self._brightness = kwargs[ATTR_BRIGHTNESS]
+
+ # Calculate pilight brightness (as a range of 0 to 15)
+ # By creating a percentage
+ percentage = self._brightness / 255
+ # Then calculate the dimmer range (aka amount of available brightness steps).
+ dimrange = self._dimlevel_max - self._dimlevel_min
+ # Finally calculate the pilight brightness.
+ # We add dimlevel_min back in to ensure the minimum is always reached.
+ dimlevel = int(percentage * dimrange + self._dimlevel_min)
self.set_state(turn_on=True, dimlevel=dimlevel)
diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py
index ac73da0a13ffa0..afbfe80b43f36c 100644
--- a/homeassistant/components/ping/binary_sensor.py
+++ b/homeassistant/components/ping/binary_sensor.py
@@ -10,7 +10,11 @@
from icmplib import SocketPermissionError, ping as icmp_ping
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import setup_reload_service
@@ -30,7 +34,6 @@
DEFAULT_NAME = "Ping"
DEFAULT_PING_COUNT = 5
-DEFAULT_DEVICE_CLASS = "connectivity"
SCAN_INTERVAL = timedelta(minutes=5)
@@ -94,7 +97,7 @@ def name(self) -> str:
@property
def device_class(self) -> str:
"""Return the class of this sensor."""
- return DEFAULT_DEVICE_CLASS
+ return DEVICE_CLASS_CONNECTIVITY
@property
def is_on(self) -> bool:
@@ -210,7 +213,8 @@ async def async_ping(self):
out_error,
)
- if pinger.returncode != 0:
+ if pinger.returncode > 1:
+ # returncode of 1 means the host is unreachable
_LOGGER.exception(
"Error running command: `%s`, return code: %s",
" ".join(self._ping_cmd),
diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json
index f78943ca941877..e03d2508037012 100644
--- a/homeassistant/components/plaato/strings.json
+++ b/homeassistant/components/plaato/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Plaato Airlock."
},
"create_entry": {
diff --git a/homeassistant/components/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json
index a0e610e452794a..5f872379d294cc 100644
--- a/homeassistant/components/plaato/translations/ca.json
+++ b/homeassistant/components/plaato/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Plaato Airlock.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/plaato/translations/el.json b/homeassistant/components/plaato/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/plaato/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/en.json b/homeassistant/components/plaato/translations/en.json
index 42d37f042fb00f..34bd9fad511320 100644
--- a/homeassistant/components/plaato/translations/en.json
+++ b/homeassistant/components/plaato/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Plaato Airlock.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json
index 90ad53085f1193..bce201af055523 100644
--- a/homeassistant/components/plaato/translations/es.json
+++ b/homeassistant/components/plaato/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Plaato Airlock.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Plaato Airlock.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/plaato/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json
index 79ea8c05b49926..cd8d46fa0c439c 100644
--- a/homeassistant/components/plaato/translations/fr.json
+++ b/homeassistant/components/plaato/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Plaato Airlock.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json
index d0f04bca9d991e..798aa63df5d4b4 100644
--- a/homeassistant/components/plaato/translations/it.json
+++ b/homeassistant/components/plaato/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Plaato Airlook.",
- "one_instance_allowed": "\u00c8 necessaria solo una singola istanza."
+ "one_instance_allowed": "\u00c8 necessaria solo una singola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli."
diff --git a/homeassistant/components/plaato/translations/lb.json b/homeassistant/components/plaato/translations/lb.json
index 5bded3676dbf36..61fa40ebb5a66c 100644
--- a/homeassistant/components/plaato/translations/lb.json
+++ b/homeassistant/components/plaato/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Plaato Airlock Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Plaato Airlock ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json
index 4f771b7b963eba..939c812348915e 100644
--- a/homeassistant/components/plaato/translations/no.json
+++ b/homeassistant/components/plaato/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Plaato Airlock.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer."
diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json
index 2e6e876e89688a..d9f7a5711373c5 100644
--- a/homeassistant/components/plaato/translations/ru.json
+++ b/homeassistant/components/plaato/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Plaato Airlock.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\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]({docs_url}) \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."
diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json
index b374a3111e8aab..51a8bfcae19cbd 100644
--- a/homeassistant/components/plaato/translations/zh-Hant.json
+++ b/homeassistant/components/plaato/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py
index d78b12c06e0fa2..1cb2416d12a113 100644
--- a/homeassistant/components/plant/__init__.py
+++ b/homeassistant/components/plant/__init__.py
@@ -12,6 +12,7 @@
ATTR_UNIT_OF_MEASUREMENT,
CONDUCTIVITY,
CONF_SENSORS,
+ LIGHT_LUX,
PERCENTAGE,
STATE_OK,
STATE_PROBLEM,
@@ -153,7 +154,7 @@ class Plant(Entity):
"max": CONF_MAX_CONDUCTIVITY,
},
READING_BRIGHTNESS: {
- ATTR_UNIT_OF_MEASUREMENT: "lux",
+ ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX,
"min": CONF_MIN_BRIGHTNESS,
"max": CONF_MAX_BRIGHTNESS,
},
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index 4e5abad4f79b85..f0052ef88301a9 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -5,7 +5,14 @@
import logging
import plexapi.exceptions
-from plexwebsocket import PlexWebsocket
+from plexwebsocket import (
+ SIGNAL_CONNECTION_STATE,
+ SIGNAL_DATA,
+ STATE_CONNECTED,
+ STATE_DISCONNECTED,
+ STATE_STOPPED,
+ PlexWebsocket,
+)
import requests.exceptions
import voluptuous as vol
@@ -14,12 +21,15 @@
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
)
+from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONF_SOURCE,
CONF_URL,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
)
+from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -75,7 +85,11 @@ async def async_setup_entry(hass, entry):
hass.config_entries.async_update_entry(entry, options=options)
plex_server = PlexServer(
- hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options
+ hass,
+ server_config,
+ entry.data[CONF_SERVER_IDENTIFIER],
+ entry.options,
+ entry.entry_id,
)
try:
await hass.async_add_executor_job(plex_server.connect)
@@ -89,15 +103,28 @@ async def async_setup_entry(hass, entry):
entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data}
)
except requests.exceptions.ConnectionError as error:
+ if entry.state != ENTRY_STATE_SETUP_RETRY:
+ _LOGGER.error(
+ "Plex server (%s) could not be reached: [%s]",
+ server_config[CONF_URL],
+ error,
+ )
+ raise ConfigEntryNotReady from error
+ except plexapi.exceptions.Unauthorized:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ PLEX_DOMAIN,
+ context={CONF_SOURCE: SOURCE_REAUTH},
+ data=entry.data,
+ )
+ )
_LOGGER.error(
- "Plex server (%s) could not be reached: [%s]",
- server_config[CONF_URL],
- error,
+ "Token not accepted, please reauthenticate Plex server '%s'",
+ entry.data[CONF_SERVER],
)
- raise ConfigEntryNotReady from error
+ return False
except (
plexapi.exceptions.BadRequest,
- plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound,
) as error:
_LOGGER.error(
@@ -124,13 +151,36 @@ async def async_setup_entry(hass, entry):
hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, [])
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
- def update_plex():
- async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
+ @callback
+ def plex_websocket_callback(signal, data, error):
+ """Handle callbacks from plexwebsocket library."""
+ if signal == SIGNAL_CONNECTION_STATE:
+
+ if data == STATE_CONNECTED:
+ _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER])
+ elif data == STATE_DISCONNECTED:
+ _LOGGER.debug(
+ "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER]
+ )
+ # Stopped websockets without errors are expected during shutdown and ignored
+ elif data == STATE_STOPPED and error:
+ _LOGGER.error(
+ "Websocket to %s failed, aborting [Error: %s]",
+ entry.data[CONF_SERVER],
+ error,
+ )
+ hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
+
+ elif signal == SIGNAL_DATA:
+ async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
session = async_get_clientsession(hass)
verify_ssl = server_config.get(CONF_VERIFY_SSL)
websocket = PlexWebsocket(
- plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl
+ plex_server.plex_server,
+ plex_websocket_callback,
+ session=session,
+ verify_ssl=verify_ssl,
)
hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket
@@ -207,7 +257,10 @@ async def async_unload_entry(hass, entry):
async def async_options_updated(hass, entry):
"""Triggered by config entry options updates."""
server_id = entry.data[CONF_SERVER_IDENTIFIER]
- hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
+
+ # Guard incomplete setup during reauth flows
+ if server_id in hass.data[PLEX_DOMAIN][SERVERS]:
+ hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
def play_on_sonos(hass, service_call):
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index ffadba63d3a220..bdbdc9c6cc9ae7 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -16,6 +16,7 @@
CONF_CLIENT_ID,
CONF_HOST,
CONF_PORT,
+ CONF_SOURCE,
CONF_SSL,
CONF_TOKEN,
CONF_URL,
@@ -70,7 +71,7 @@ async def async_discover(hass):
for server_data in gdm.entries:
await hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
+ context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY},
data=server_data,
)
@@ -209,10 +210,6 @@ async def async_step_server_validate(self, server_config):
return await self.async_step_user(errors=errors)
server_id = plex_server.machine_identifier
-
- await self.async_set_unique_id(server_id)
- self._abort_if_unique_id_configured()
-
url = plex_server.url_in_use
token = server_config.get(CONF_TOKEN)
@@ -226,16 +223,27 @@ async def async_step_server_validate(self, server_config):
CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL
)
+ data = {
+ CONF_SERVER: plex_server.friendly_name,
+ CONF_SERVER_IDENTIFIER: server_id,
+ PLEX_SERVER_CONFIG: entry_config,
+ }
+
+ entry = await self.async_set_unique_id(server_id)
+ if (
+ self.context[CONF_SOURCE] # pylint: disable=no-member
+ == config_entries.SOURCE_REAUTH
+ ):
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ self._abort_if_unique_id_configured()
+
_LOGGER.debug("Valid config created for %s", plex_server.friendly_name)
- return self.async_create_entry(
- title=plex_server.friendly_name,
- data={
- CONF_SERVER: plex_server.friendly_name,
- CONF_SERVER_IDENTIFIER: server_id,
- PLEX_SERVER_CONFIG: entry_config,
- },
- )
+ return self.async_create_entry(title=plex_server.friendly_name, data=data)
async def async_step_select_server(self, user_input=None):
"""Use selected Plex server."""
@@ -316,6 +324,11 @@ async def async_step_use_external_token(self, user_input=None):
server_config = {CONF_TOKEN: self.token}
return await self.async_step_server_validate(server_config)
+ async def async_step_reauth(self, data):
+ """Handle a reauthorization flow request."""
+ self.current_login = dict(data)
+ return await self.async_step_user()
+
class PlexOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Plex options."""
diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json
index bbf7be9914ef80..f5bbc6ac53c2ed 100644
--- a/homeassistant/components/plex/manifest.json
+++ b/homeassistant/components/plex/manifest.json
@@ -4,9 +4,9 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plex",
"requirements": [
- "plexapi==4.1.0",
+ "plexapi==4.1.1",
"plexauth==0.0.5",
- "plexwebsocket==0.0.11"
+ "plexwebsocket==0.0.12"
],
"dependencies": ["http"],
"after_dependencies": ["sonos"],
diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py
index 28444c4a3514c0..56e6f68a9686a3 100644
--- a/homeassistant/components/plex/media_browser.py
+++ b/homeassistant/components/plex/media_browser.py
@@ -103,7 +103,7 @@ def build_item_response(payload):
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
except KeyError as err:
raise BrowseError(
- f"Media not found: {media_content_type} / {media_content_id}"
+ f"Unknown type received: {library_or_section.TYPE}"
) from err
else:
raise BrowseError(
@@ -205,6 +205,7 @@ def special_library_payload(parent_payload, special_type):
media_content_type=parent_payload.media_content_type,
can_play=False,
can_expand=True,
+ children_media_class=parent_payload.children_media_class,
)
@@ -217,9 +218,9 @@ def server_payload(plex_server):
media_content_type="server",
can_play=False,
can_expand=True,
+ children_media_class=MEDIA_CLASS_DIRECTORY,
)
server_info.children = []
- server_info.children_media_class = MEDIA_CLASS_DIRECTORY
server_info.children.append(special_library_payload(server_info, "On Deck"))
server_info.children.append(special_library_payload(server_info, "Recently Added"))
for library in plex_server.library.sections():
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index f5dc98e4eb1d61..94bed1db7debbd 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -597,7 +597,7 @@ def play_media(self, media_type, media_id, **kwargs):
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
- attr = {
+ return {
"media_content_rating": self._media_content_rating,
"session_username": self.username,
"media_library_name": self._app_name,
@@ -605,8 +605,6 @@ def device_state_attributes(self):
"player_source": self.player_source,
}
- return attr
-
@property
def device_info(self):
"""Return a device description for device registry."""
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index f8706eadf22599..a5ac287328e3c2 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -62,9 +62,12 @@
class PlexServer:
"""Manages a single Plex server connection."""
- def __init__(self, hass, server_config, known_server_id=None, options=None):
+ def __init__(
+ self, hass, server_config, known_server_id=None, options=None, entry_id=None
+ ):
"""Initialize a Plex server instance."""
self.hass = hass
+ self.entry_id = entry_id
self._plex_account = None
self._plex_server = None
self._created_clients = set()
@@ -270,6 +273,12 @@ async def _async_update_platforms(self):
devices, sessions, plextv_clients = await self.hass.async_add_executor_job(
self._fetch_platform_data
)
+ except plexapi.exceptions.Unauthorized:
+ _LOGGER.debug(
+ "Token has expired for '%s', reloading integration", self.friendly_name
+ )
+ await self.hass.config_entries.async_reload(self.entry_id)
+ return
except (
plexapi.exceptions.BadRequest,
requests.exceptions.RequestException,
diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json
index 2f50e2d30908dc..bfe6375f5ac5cc 100644
--- a/homeassistant/components/plex/strings.json
+++ b/homeassistant/components/plex/strings.json
@@ -17,8 +17,8 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
- "ssl": "Use SSL",
- "verify_ssl": "Verify SSL certificate",
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"token": "Token (Optional)"
}
},
@@ -40,9 +40,10 @@
"abort": {
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
- "already_in_progress": "Plex is being configured",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"token_request_timeout": "Timed out obtaining token",
- "unknown": "Failed for unknown reason"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json
index be4b6215f8b656..32fcb976c38fef 100644
--- a/homeassistant/components/plex/translations/ca.json
+++ b/homeassistant/components/plex/translations/ca.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats",
"already_configured": "Aquest servidor Plex ja est\u00e0 configurat",
- "already_in_progress": "S'est\u00e0 configurant Plex",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "reauth_successful": "Re-autenticaci\u00f3 exitosa",
"token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del token.",
- "unknown": "Ha fallat per motiu desconegut"
+ "unknown": "Error inesperat"
},
"error": {
"faulty_credentials": "Ha fallat l'autoritzaci\u00f3, comprova el Token",
@@ -20,7 +21,7 @@
"data": {
"host": "Amfitri\u00f3",
"port": "Port",
- "ssl": "Utilitza SSL",
+ "ssl": "Utilitza un certificat SSL",
"token": "Token (opcional)",
"verify_ssl": "Verifica el certificat SSL"
},
diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json
index b14e3a3c574458..961ad4b3ed6e3e 100644
--- a/homeassistant/components/plex/translations/de.json
+++ b/homeassistant/components/plex/translations/de.json
@@ -14,6 +14,7 @@
"not_found": "Plex-Server nicht gefunden",
"ssl_error": "SSL-Zertifikatsproblem"
},
+ "flow_title": "{name} ({host})",
"step": {
"manual_setup": {
"data": {
diff --git a/homeassistant/components/plex/translations/el.json b/homeassistant/components/plex/translations/el.json
new file mode 100644
index 00000000000000..54f64b814fd6cf
--- /dev/null
+++ b/homeassistant/components/plex/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/translations/en.json b/homeassistant/components/plex/translations/en.json
index 83e5196fc357fe..a278fe2224d000 100644
--- a/homeassistant/components/plex/translations/en.json
+++ b/homeassistant/components/plex/translations/en.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
- "already_in_progress": "Plex is being configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "reauth_successful": "Successfully reauthenticated",
"token_request_timeout": "Timed out obtaining token",
- "unknown": "Failed for unknown reason"
+ "unknown": "Unexpected error"
},
"error": {
"faulty_credentials": "Authorization failed, verify Token",
@@ -20,7 +21,7 @@
"data": {
"host": "Host",
"port": "Port",
- "ssl": "Use SSL",
+ "ssl": "Uses an SSL certificate",
"token": "Token (Optional)",
"verify_ssl": "Verify SSL certificate"
},
diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json
index 907025590c6efc..cc5f4569020000 100644
--- a/homeassistant/components/plex/translations/es.json
+++ b/homeassistant/components/plex/translations/es.json
@@ -4,6 +4,7 @@
"all_configured": "Todos los servidores vinculados ya configurados",
"already_configured": "Este servidor Plex ya est\u00e1 configurado",
"already_in_progress": "Plex se est\u00e1 configurando",
+ "reauth_successful": "Se ha vuelto a autenticar con \u00e9xito",
"token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token",
"unknown": "Fall\u00f3 por razones desconocidas"
},
diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json
index b0996fad3d7aa9..ce487d6ec27449 100644
--- a/homeassistant/components/plex/translations/it.json
+++ b/homeassistant/components/plex/translations/it.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Plex \u00e8 in fase di configurazione",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "reauth_successful": "Ri-autenticato con successo",
"token_request_timeout": "Timeout per l'ottenimento del token",
- "unknown": "Non riuscito per motivo sconosciuto"
+ "unknown": "Errore imprevisto"
},
"error": {
"faulty_credentials": "Autorizzazione non riuscita, verificare il Token",
@@ -20,7 +21,7 @@
"data": {
"host": "Host",
"port": "Porta",
- "ssl": "Utilizzare SSL",
+ "ssl": "Utilizza un certificato SSL",
"token": "Token (opzionale)",
"verify_ssl": "Verificare il certificato SSL"
},
diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json
index 3a01e3f67c1cdd..eda0cb04b0384e 100644
--- a/homeassistant/components/plex/translations/lb.json
+++ b/homeassistant/components/plex/translations/lb.json
@@ -4,6 +4,7 @@
"all_configured": "All verbonne Server sinn scho konfigur\u00e9iert",
"already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert",
"already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert",
+ "reauth_successful": "Erfollegr\u00e4ich re-authentifiz\u00e9iert",
"token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton",
"unknown": "Onbekannte Feeler opgetrueden"
},
diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json
index c7374e27b60174..33ed22fdb231c6 100644
--- a/homeassistant/components/plex/translations/no.json
+++ b/homeassistant/components/plex/translations/no.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "Alle knyttet servere som allerede er konfigurert",
"already_configured": "Denne Plex-serveren er allerede konfigurert",
- "already_in_progress": "Plex blir konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "reauth_successful": "Godkjent p\u00e5 nytt",
"token_request_timeout": "Tidsavbrudd ved innhenting av token",
- "unknown": "Mislyktes av ukjent \u00e5rsak"
+ "unknown": "Uventet feil"
},
"error": {
"faulty_credentials": "Autorisasjonen mislyktes, bekreft token",
@@ -20,7 +21,7 @@
"data": {
"host": "Vert",
"port": "",
- "ssl": "Bruk SSL",
+ "ssl": "Bruker et SSL-sertifikat",
"token": "Token (valgfritt)",
"verify_ssl": "Verifisere SSL-sertifikat"
},
diff --git a/homeassistant/components/plex/translations/pl.json b/homeassistant/components/plex/translations/pl.json
index a1d4ee96a1bda0..9934c848f18451 100644
--- a/homeassistant/components/plex/translations/pl.json
+++ b/homeassistant/components/plex/translations/pl.json
@@ -4,6 +4,7 @@
"all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.",
"already_configured": "Ten serwer Plex jest ju\u017c skonfigurowany.",
"already_in_progress": "Plex jest konfigurowany",
+ "reauth_successful": "Ponowne uwierzytelnianie powiod\u0142o si\u0119",
"token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.",
"unknown": "Nieznany b\u0142\u0105d"
},
diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json
index 29937ccea5fe65..62e8d49967077b 100644
--- a/homeassistant/components/plex/translations/ru.json
+++ b/homeassistant/components/plex/translations/ru.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.",
"already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.",
- "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.",
+ "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.",
+ "reauth_successful": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
"token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.",
- "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435."
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0422\u043e\u043a\u0435\u043d.",
@@ -20,7 +21,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"token": "\u0422\u043e\u043a\u0435\u043d (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
"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/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json
index 2d866880decb49..ea207f2464ef1a 100644
--- a/homeassistant/components/plex/translations/zh-Hant.json
+++ b/homeassistant/components/plex/translations/zh-Hant.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210",
"already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "reauth_successful": "\u5df2\u6210\u529f\u91cd\u65b0\u8a8d\u8b49",
"token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642",
- "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557"
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
"faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u5bc6\u9470",
@@ -20,7 +21,7 @@
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u4f7f\u7528 SSL",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"token": "\u5bc6\u9470\uff08\u9078\u9805\uff09",
"verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py
index 8c140f65af91e5..f7986f915401c6 100644
--- a/homeassistant/components/plugwise/__init__.py
+++ b/homeassistant/components/plugwise/__init__.py
@@ -10,7 +10,7 @@
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
@@ -21,7 +21,13 @@
UpdateFailed,
)
-from .const import COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, UNDO_UPDATE_LISTENER
+from .const import (
+ COORDINATOR,
+ DEFAULT_PORT,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ UNDO_UPDATE_LISTENER,
+)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
@@ -39,9 +45,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Plugwise Smiles from a config entry."""
websession = async_get_clientsession(hass, verify_ssl=False)
+
api = Smile(
host=entry.data[CONF_HOST],
password=entry.data[CONF_PASSWORD],
+ port=entry.data.get(CONF_PORT, DEFAULT_PORT),
+ timeout=30,
websession=websession,
)
@@ -94,6 +103,10 @@ async def async_update_data():
api.get_all_devices()
+ if entry.unique_id is None:
+ if api.smile_version[0] != "1.8.0":
+ hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
+
undo_listener = entry.add_update_listener(_update_listener)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index 20c8a5a216c7bb..14405062231cf2 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -5,12 +5,16 @@
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
-from .const import DEFAULT_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import
+from .const import ( # pylint:disable=unused-import
+ DEFAULT_PORT,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
@@ -27,6 +31,7 @@ def _base_schema(discovery_info):
if not discovery_info:
base_schema[vol.Required(CONF_HOST)] = str
+ base_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int
base_schema[vol.Required(CONF_PASSWORD)] = str
@@ -40,9 +45,11 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from _base_schema() with values provided by the user.
"""
websession = async_get_clientsession(hass, verify_ssl=False)
+
api = Smile(
host=data[CONF_HOST],
password=data[CONF_PASSWORD],
+ port=data[CONF_PORT],
timeout=30,
websession=websession,
)
@@ -83,6 +90,7 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_HOST: discovery_info[CONF_HOST],
+ CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT),
"name": _name,
}
return await self.async_step_user()
@@ -95,6 +103,11 @@ async def async_step_user(self, user_input=None):
if self.discovery_info:
user_input[CONF_HOST] = self.discovery_info[CONF_HOST]
+ user_input[CONF_PORT] = self.discovery_info.get(CONF_PORT, DEFAULT_PORT)
+
+ for entry in self._async_current_entries():
+ if entry.data.get(CONF_HOST) == user_input[CONF_HOST]:
+ return self.async_abort(reason="already_configured")
try:
api = await validate_input(self.hass, user_input)
@@ -107,7 +120,9 @@ async def async_step_user(self, user_input=None):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
- await self.async_set_unique_id(api.gateway_id)
+ await self.async_set_unique_id(
+ api.smile_hostname or api.gateway_id, raise_on_progress=False
+ )
self._abort_if_unique_id_configured()
return self.async_create_entry(title=api.smile_name, data=user_input)
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index f4cb9164e5d52a..222db34b34423b 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_Smile==1.4.0"],
+ "requirements": ["Plugwise_Smile==1.5.1"],
"codeowners": ["@CoMPaTech", "@bouwew"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true
diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json
index 7dc8542698bca6..6a780282a1393e 100644
--- a/homeassistant/components/plugwise/strings.json
+++ b/homeassistant/components/plugwise/strings.json
@@ -13,20 +13,21 @@
"step": {
"user": {
"title": "Connect to the Smile",
- "description": "Details",
+ "description": "Please enter:",
"data": {
- "host": "Smile IP address",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "host": "[%key:common::config_flow::data::ip%]",
+ "port": "[%key:common::config_flow::data::port%]"
}
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "This Smile is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"flow_title": "Smile: {name}"
}
diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json
index c61c28e66689f3..cabded862e272f 100644
--- a/homeassistant/components/plugwise/translations/ca.json
+++ b/homeassistant/components/plugwise/translations/ca.json
@@ -1,21 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "Smile ja est\u00e0 configurat"
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
- "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida, comprova els 8 car\u00e0cters de l'ID de Smile.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
- "host": "Adre\u00e7a IP de Smile",
- "password": "ID de Smile"
+ "host": "Adre\u00e7a IP",
+ "password": "ID de Smile",
+ "port": "Port"
},
- "description": "Detalls",
+ "description": "Introdueix:",
"title": "Connecta't amb el Smile"
}
}
diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json
index 7ee92b1ead5018..19d3678b5e8599 100644
--- a/homeassistant/components/plugwise/translations/de.json
+++ b/homeassistant/components/plugwise/translations/de.json
@@ -4,6 +4,7 @@
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json
new file mode 100644
index 00000000000000..1636ae0bef10c4
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/el.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2 \u03c7\u03b1\u03bc\u03cc\u03b3\u03b5\u03bb\u03bf\u03c5"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)"
+ },
+ "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03ad\u03c4\u03b7\u03c3\u03b7\u03c2"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json
index 238f435f3abc0f..5ee28addc6402c 100644
--- a/homeassistant/components/plugwise/translations/en.json
+++ b/homeassistant/components/plugwise/translations/en.json
@@ -1,21 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "This Smile is already configured"
+ "already_configured": "Service is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
- "host": "Smile IP address",
- "password": "Smile ID"
+ "host": "IP Address",
+ "password": "Smile ID",
+ "port": "Port"
},
- "description": "Details",
+ "description": "Please enter:",
"title": "Connect to the Smile"
}
}
diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json
index 31e876cfe3a12a..9ab00348f81988 100644
--- a/homeassistant/components/plugwise/translations/es.json
+++ b/homeassistant/components/plugwise/translations/es.json
@@ -13,11 +13,22 @@
"user": {
"data": {
"host": "Direcci\u00f3n IP de Smile",
- "password": "ID Smile"
+ "password": "ID Smile",
+ "port": "N\u00famero de puerto de Smile"
},
"description": "Detalles",
"title": "Conectarse a Smile"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalo de escaneo (segundos)"
+ },
+ "description": "Ajustar las opciones de Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json
new file mode 100644
index 00000000000000..4f7b631d2df09b
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/et.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Smile pordi number"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "P\u00e4ringute intervall (sekundites)"
+ },
+ "description": "Kohanda Plugwise s\u00e4tteid"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json
index 7ef9296193bead..fe4b88ab0f144f 100644
--- a/homeassistant/components/plugwise/translations/fr.json
+++ b/homeassistant/components/plugwise/translations/fr.json
@@ -13,9 +13,10 @@
"user": {
"data": {
"host": "Adresse IP de Smile",
- "password": "ID Smile"
+ "password": "ID Smile",
+ "port": "Num\u00e9ro de port Smile"
},
- "description": "D\u00e9tails",
+ "description": "Veuillez saisir :",
"title": "Se connecter \u00e0 Smile"
}
}
diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json
index b1b3365125a6b1..92f5d1a3426d21 100644
--- a/homeassistant/components/plugwise/translations/it.json
+++ b/homeassistant/components/plugwise/translations/it.json
@@ -1,21 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "Smile gi\u00e0 configurato"
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
- "invalid_auth": "Autenticazione non valida. Controllare gli 8 caratteri dell'ID Smile",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
- "host": "Indirizzo IP Smile",
- "password": "ID Smile"
+ "host": "Indirizzo IP",
+ "password": "ID Smile",
+ "port": "Porta"
},
- "description": "Dettagli",
+ "description": "Si prega di inserire:",
"title": "Connettersi al dispositivo"
}
}
diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json
index df480242f6f3d8..04fcb73828528f 100644
--- a/homeassistant/components/plugwise/translations/ko.json
+++ b/homeassistant/components/plugwise/translations/ko.json
@@ -13,11 +13,22 @@
"user": {
"data": {
"host": "Smile IP \uc8fc\uc18c",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "port": "\uc2a4\ub9c8\uc77c \ud3ec\ud2b8 \ubc88\ud638"
},
"description": "\uc138\ubd80 \uc815\ubcf4",
"title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
+ },
+ "description": "Plugwise \uc635\uc158 \uc870\uc815"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json
index 8b0ea38c2f6095..cd2100804a0a31 100644
--- a/homeassistant/components/plugwise/translations/lb.json
+++ b/homeassistant/components/plugwise/translations/lb.json
@@ -13,11 +13,22 @@
"user": {
"data": {
"host": "Smile IP Adresse",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "port": "Smile Port Nummer"
},
"description": "Detailler",
"title": "Mat Smile verbannen"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Scan Intervall (sekonnen)"
+ },
+ "description": "Plugwise Optioune \u00e4nneren"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json
index 964675e0c631e2..5d0bd789957160 100644
--- a/homeassistant/components/plugwise/translations/nl.json
+++ b/homeassistant/components/plugwise/translations/nl.json
@@ -18,5 +18,14 @@
"title": "Maak verbinding met de Smile"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Scaninterval (seconden)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json
index 2ee14f0e1525cf..4902ada06c2052 100644
--- a/homeassistant/components/plugwise/translations/no.json
+++ b/homeassistant/components/plugwise/translations/no.json
@@ -13,9 +13,10 @@
"user": {
"data": {
"host": "Smile IP-adresse",
- "password": ""
+ "password": "",
+ "port": "Smil portnummer"
},
- "description": "Detaljer",
+ "description": "Vennligst skriv inn:",
"title": "Koble til Smile"
}
}
diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json
index 135b9d838fcfba..473b02c9b3c689 100644
--- a/homeassistant/components/plugwise/translations/pl.json
+++ b/homeassistant/components/plugwise/translations/pl.json
@@ -6,18 +6,29 @@
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
"invalid_auth": "Nieudane uwierzytelnienie, sprawd\u017a Smile ID",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
"host": "Adres IP Smile",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "port": "Numer portu dla Smile"
},
"description": "Szczeg\u00f3\u0142y",
"title": "Po\u0142\u0105cz si\u0119 ze Smile"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)"
+ },
+ "description": "Dostosowywanie opcji Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json
index 1c4f8ded80cf57..5d3afb061fc581 100644
--- a/homeassistant/components/plugwise/translations/ru.json
+++ b/homeassistant/components/plugwise/translations/ru.json
@@ -13,7 +13,8 @@
"user": {
"data": {
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
- "password": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
+ "password": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
+ "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 Smile"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Plugwise.",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json
index ee6966c1bdbe6d..58dd71266b3c42 100644
--- a/homeassistant/components/plugwise/translations/zh-Hant.json
+++ b/homeassistant/components/plugwise/translations/zh-Hant.json
@@ -13,9 +13,10 @@
"user": {
"data": {
"host": "Smile IP \u4f4d\u5740",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "port": "Smile \u901a\u8a0a\u57e0"
},
- "description": "\u8a73\u7d30\u8cc7\u8a0a",
+ "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\uff1a",
"title": "\u9023\u7dda\u81f3 Smile"
}
}
diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json
new file mode 100644
index 00000000000000..f55df964f86cbe
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "E-Mail"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/hu.json b/homeassistant/components/plum_lightpad/translations/hu.json
new file mode 100644
index 00000000000000..436e8b1fb7dd75
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/pl.json b/homeassistant/components/plum_lightpad/translations/pl.json
index 121744d0f0dcef..83d814d65dcc57 100644
--- a/homeassistant/components/plum_lightpad/translations/pl.json
+++ b/homeassistant/components/plum_lightpad/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"user": {
diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py
index 5a780c2e57a359..d82ecd096ee2c8 100644
--- a/homeassistant/components/point/binary_sensor.py
+++ b/homeassistant/components/point/binary_sensor.py
@@ -1,7 +1,11 @@
"""Support for Minut Point binary sensors."""
import logging
-from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DOMAIN,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -116,7 +120,7 @@ def _webhook_event(self, data, webhook):
@property
def is_on(self):
"""Return the state of the binary sensor."""
- if self.device_class == "connectivity":
+ if self.device_class == DEVICE_CLASS_CONNECTIVITY:
# connectivity is the other way around.
return not self._is_on
return self._is_on
diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py
index 4ac8f0c1832cba..9436877e434e80 100644
--- a/homeassistant/components/point/sensor.py
+++ b/homeassistant/components/point/sensor.py
@@ -7,6 +7,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
+ PRESSURE_HPA,
TEMP_CELSIUS,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -21,7 +22,7 @@
SENSOR_TYPES = {
DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS),
- DEVICE_CLASS_PRESSURE: (None, 0, "hPa"),
+ DEVICE_CLASS_PRESSURE: (None, 0, PRESSURE_HPA),
DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE),
DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, "dBa"),
}
diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json
index 184a7c9df581be..d4126e9077118f 100644
--- a/homeassistant/components/point/translations/ca.json
+++ b/homeassistant/components/point/translations/ca.json
@@ -3,7 +3,7 @@
"abort": {
"already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
"authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.",
- "authorize_url_timeout": "Temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3 esgotat.",
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.",
"no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3."
},
diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json
index a7247b3d9b3677..5374d2808d9a0b 100644
--- a/homeassistant/components/point/translations/es.json
+++ b/homeassistant/components/point/translations/es.json
@@ -23,7 +23,7 @@
"data": {
"flow_impl": "Proveedor"
},
- "description": "\u00bfQuieres comenzar a configurar?",
+ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?",
"title": "Selecciona el m\u00e9todo de autenticaci\u00f3n"
}
}
diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json
index 1596ba05916a3c..286c9e67fc88e5 100644
--- a/homeassistant/components/point/translations/pl.json
+++ b/homeassistant/components/point/translations/pl.json
@@ -12,7 +12,7 @@
},
"error": {
"follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku \"Zatwierd\u017a\"",
- "no_token": "Niepoprawny token dost\u0119pu."
+ "no_token": "Niepoprawny token dost\u0119pu"
},
"step": {
"auth": {
diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json
index d54c09e3cef470..8fc660e8128f2a 100644
--- a/homeassistant/components/poolsense/translations/de.json
+++ b/homeassistant/components/poolsense/translations/de.json
@@ -6,6 +6,7 @@
"step": {
"user": {
"data": {
+ "email": "E-Mail",
"password": "Passwort"
}
}
diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/poolsense/translations/pl.json b/homeassistant/components/poolsense/translations/pl.json
index d463be1c5dd4bd..521be99aa40cd5 100644
--- a/homeassistant/components/poolsense/translations/pl.json
+++ b/homeassistant/components/poolsense/translations/pl.json
@@ -1,17 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
"data": {
"email": "Adres e-mail",
"password": "[%key_id:common::config_flow::data::password%]"
- }
+ },
+ "title": "PoolSense"
}
}
}
diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json
index ce7e6a1965fa90..86b7db3e9092e3 100644
--- a/homeassistant/components/powerwall/strings.json
+++ b/homeassistant/components/powerwall/strings.json
@@ -3,14 +3,18 @@
"step": {
"user": {
"title": "Connect to the powerwall",
- "data": { "ip_address": "IP Address" }
+ "data": {
+ "ip_address": "[%key:common::config_flow::data::ip%]"
+ }
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.",
- "unknown": "Unexpected error"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
- "abort": { "already_configured": "The powerwall is already configured" }
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
}
}
diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json
index b0764b78234600..2c9becb17959e6 100644
--- a/homeassistant/components/powerwall/translations/ca.json
+++ b/homeassistant/components/powerwall/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "El Powerwall ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"unknown": "Error inesperat",
"wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat."
},
diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json
index 8b45c665d858d5..773daace5c0bef 100644
--- a/homeassistant/components/powerwall/translations/en.json
+++ b/homeassistant/components/powerwall/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "The powerwall is already configured"
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"unknown": "Unexpected error",
"wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved."
},
diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json
index 09a3d5708014fa..422a28b6936958 100644
--- a/homeassistant/components/powerwall/translations/it.json
+++ b/homeassistant/components/powerwall/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Il Powerwall \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"unknown": "Errore imprevisto",
"wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto."
},
diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json
index a688a9ad8c4472..6690bb08efba38 100644
--- a/homeassistant/components/powerwall/translations/no.json
+++ b/homeassistant/components/powerwall/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Powerwall er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"unknown": "Uventet feil",
"wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses."
},
diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json
index 20eb71e7c277d2..cc56f1e116dde2 100644
--- a/homeassistant/components/powerwall/translations/pl.json
+++ b/homeassistant/components/powerwall/translations/pl.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d",
"wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107."
},
"step": {
diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json
index cf3b1cd4ba04af..a8713bcd04ae31 100644
--- a/homeassistant/components/powerwall/translations/ru.json
+++ b/homeassistant/components/powerwall/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c."
},
diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json
index 8883e669a8592b..8cfa8bdb46b5f3 100644
--- a/homeassistant/components/powerwall/translations/zh-Hant.json
+++ b/homeassistant/components/powerwall/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Powerwall \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4",
"wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002"
},
diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py
new file mode 100644
index 00000000000000..3c989e13eeb458
--- /dev/null
+++ b/homeassistant/components/profiler/__init__.py
@@ -0,0 +1,83 @@
+"""The profiler integration."""
+import asyncio
+import cProfile
+import logging
+import time
+
+from pyprof2calltree import convert
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.helpers.service import async_register_admin_service
+from homeassistant.helpers.typing import ConfigType
+
+from .const import DOMAIN
+
+SERVICE_START = "start"
+CONF_SECONDS = "seconds"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the profiler component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Profiler from a config entry."""
+
+ lock = asyncio.Lock()
+
+ async def _async_run_profile(call: ServiceCall):
+ async with lock:
+ await _async_generate_profile(hass, call)
+
+ async_register_admin_service(
+ hass,
+ DOMAIN,
+ SERVICE_START,
+ _async_run_profile,
+ schema=vol.Schema(
+ {vol.Optional(CONF_SECONDS, default=60.0): vol.Coerce(float)}
+ ),
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ hass.services.async_remove(domain=DOMAIN, service=SERVICE_START)
+ return True
+
+
+async def _async_generate_profile(hass: HomeAssistant, call: ServiceCall):
+ start_time = int(time.time() * 1000000)
+ hass.components.persistent_notification.async_create(
+ "The profile started. This notification will be updated when it is complete.",
+ title="Profile Started",
+ notification_id=f"profiler_{start_time}",
+ )
+ profiler = cProfile.Profile()
+ profiler.enable()
+ await asyncio.sleep(float(call.data[CONF_SECONDS]))
+ profiler.disable()
+
+ cprofile_path = hass.config.path(f"profile.{start_time}.cprof")
+ callgrind_path = hass.config.path(f"callgrind.out.{start_time}")
+ await hass.async_add_executor_job(
+ _write_profile, profiler, cprofile_path, callgrind_path
+ )
+ hass.components.persistent_notification.async_create(
+ f"Wrote cProfile data to {cprofile_path} and callgrind data to {callgrind_path}",
+ title="Profile Complete",
+ notification_id=f"profiler_{start_time}",
+ )
+
+
+def _write_profile(profiler, cprofile_path, callgrind_path):
+ profiler.create_stats()
+ profiler.dump_stats(cprofile_path)
+ convert(profiler.getstats(), callgrind_path)
diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py
new file mode 100644
index 00000000000000..73f4f86255b04e
--- /dev/null
+++ b/homeassistant/components/profiler/config_flow.py
@@ -0,0 +1,28 @@
+"""Config flow for Profiler integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+
+from .const import DEFAULT_NAME
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Profiler."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
+
+ 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")
+
+ 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({}))
diff --git a/homeassistant/components/profiler/const.py b/homeassistant/components/profiler/const.py
new file mode 100644
index 00000000000000..ee80a9175f861d
--- /dev/null
+++ b/homeassistant/components/profiler/const.py
@@ -0,0 +1,4 @@
+"""Consts used by profiler."""
+
+DOMAIN = "profiler"
+DEFAULT_NAME = "Profiler"
diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json
new file mode 100644
index 00000000000000..e740a083c77096
--- /dev/null
+++ b/homeassistant/components/profiler/manifest.json
@@ -0,0 +1,13 @@
+{
+ "domain": "profiler",
+ "name": "Profiler",
+ "documentation": "https://www.home-assistant.io/integrations/profiler",
+ "requirements": [
+ "pyprof2calltree==1.4.5"
+ ],
+ "codeowners": [
+ "@bdraco"
+ ],
+ "quality_scale": "internal",
+ "config_flow": true
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml
new file mode 100644
index 00000000000000..7033e988fc567a
--- /dev/null
+++ b/homeassistant/components/profiler/services.yaml
@@ -0,0 +1,6 @@
+start:
+ description: Start the Profiler
+ fields:
+ seconds:
+ description: The number of seconds to run the profiler.
+ example: 60.0
diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json
new file mode 100644
index 00000000000000..80adf97390216d
--- /dev/null
+++ b/homeassistant/components/profiler/strings.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Are you sure you want to set up the Profiler?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ }
+ }
+}
diff --git a/homeassistant/components/profiler/translations/ca.json b/homeassistant/components/profiler/translations/ca.json
new file mode 100644
index 00000000000000..1e6c1c03cf2462
--- /dev/null
+++ b/homeassistant/components/profiler/translations/ca.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "step": {
+ "user": {
+ "description": "Est\u00e0s segur que vols configurar Profiler?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/en.json b/homeassistant/components/profiler/translations/en.json
new file mode 100644
index 00000000000000..8a858322445c82
--- /dev/null
+++ b/homeassistant/components/profiler/translations/en.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "step": {
+ "user": {
+ "description": "Are you sure you want to set up the Profiler?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/es.json b/homeassistant/components/profiler/translations/es.json
new file mode 100644
index 00000000000000..c9a6215892e9fc
--- /dev/null
+++ b/homeassistant/components/profiler/translations/es.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
+ },
+ "step": {
+ "user": {
+ "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Profiler?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/et.json b/homeassistant/components/profiler/translations/et.json
new file mode 100644
index 00000000000000..553790181c3410
--- /dev/null
+++ b/homeassistant/components/profiler/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "step": {
+ "user": {
+ "description": "Kas soovid h\u00e4\u00e4lestada Profiler'i sidumist?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/it.json b/homeassistant/components/profiler/translations/it.json
new file mode 100644
index 00000000000000..7829a7c3017eaa
--- /dev/null
+++ b/homeassistant/components/profiler/translations/it.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "step": {
+ "user": {
+ "description": "Sei sicuro di voler configurare il Profiler?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/ru.json b/homeassistant/components/profiler/translations/ru.json
new file mode 100644
index 00000000000000..1b91f5e62beb5c
--- /dev/null
+++ b/homeassistant/components/profiler/translations/ru.json
@@ -0,0 +1,12 @@
+{
+ "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": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Profiler?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json
new file mode 100644
index 00000000000000..f772a8586d00c0
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/de.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Relais 1",
+ "relay_10": "Relais 10",
+ "relay_11": "Relais 11",
+ "relay_12": "Relais 12",
+ "relay_13": "Relais 13",
+ "relay_14": "Relais 14",
+ "relay_15": "Relais 15",
+ "relay_16": "Relais 16",
+ "relay_2": "Relais 2",
+ "relay_3": "Relais 3",
+ "relay_4": "Relais 4",
+ "relay_5": "Relais 5",
+ "relay_6": "Relais 6",
+ "relay_7": "Relais 7",
+ "relay_8": "Relais 8",
+ "relay_9": "Relais 9"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/el.json b/homeassistant/components/progettihwsw/translations/el.json
new file mode 100644
index 00000000000000..76109815009215
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/el.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_10": "\u03a1\u03b5\u03bb\u03ad 10",
+ "relay_11": "\u03a1\u03b5\u03bb\u03ad 11",
+ "relay_12": "\u03a1\u03b5\u03bb\u03ad 12",
+ "relay_13": "\u03a1\u03b5\u03bb\u03ad 13",
+ "relay_14": "\u03a1\u03b5\u03bb\u03ad 14",
+ "relay_15": "\u03a1\u03b5\u03bb\u03ad 15",
+ "relay_16": "\u03a1\u03b5\u03bb\u03ad 16",
+ "relay_2": "\u03a1\u03b5\u03bb\u03ad 2",
+ "relay_3": "\u03a1\u03b5\u03bb\u03ad 3",
+ "relay_4": "\u03a1\u03b5\u03bb\u03ad 4",
+ "relay_5": "\u03a1\u03b5\u03bb\u03ad 5",
+ "relay_6": "\u03a1\u03b5\u03bb\u03ad 6",
+ "relay_7": "\u03a1\u03b5\u03bb\u03ad 7",
+ "relay_8": "\u03a1\u03b5\u03bb\u03ad 8",
+ "relay_9": "\u03a1\u03b5\u03bb\u03ad 9"
+ },
+ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad"
+ },
+ "user": {
+ "data": {
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1"
+ }
+ }
+ },
+ "title": "\u0391\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 ProgettiHWSW"
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/ko.json b/homeassistant/components/progettihwsw/translations/ko.json
new file mode 100644
index 00000000000000..b8b78de069c375
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/ko.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec",
+ "wrong_info_relay_modes": "\ub9b4\ub808\uc774 \ubaa8\ub4dc \uc120\ud0dd\uc740 Monostable \ub610\ub294 Bistable \uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "\ub9b4\ub808\uc774 1",
+ "relay_10": "\ub9b4\ub808\uc774 10",
+ "relay_11": "\ub9b4\ub808\uc774 11",
+ "relay_12": "\ub9b4\ub808\uc774 12",
+ "relay_13": "\ub9b4\ub808\uc774 13",
+ "relay_14": "\ub9b4\ub808\uc774 14",
+ "relay_15": "\ub9b4\ub808\uc774 15",
+ "relay_16": "\ub9b4\ub808\uc774 16",
+ "relay_2": "\ub9b4\ub808\uc774 2",
+ "relay_3": "\ub9b4\ub808\uc774 3",
+ "relay_4": "\ub9b4\ub808\uc774 4",
+ "relay_5": "\ub9b4\ub808\uc774 5",
+ "relay_6": "\ub9b4\ub808\uc774 6",
+ "relay_7": "\ub9b4\ub808\uc774 7",
+ "relay_8": "\ub9b4\ub808\uc774 8",
+ "relay_9": "\ub9b4\ub808\uc774 9"
+ },
+ "title": "\ub9b4\ub808\uc774 \uc124\uc815"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ },
+ "title": "\ubcf4\ub4dc \uc124\uc815"
+ }
+ }
+ },
+ "title": "ProgettiHWSW \uc790\ub3d9\ud654"
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/lb.json b/homeassistant/components/progettihwsw/translations/lb.json
new file mode 100644
index 00000000000000..2e5491da644070
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/lb.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Relais 1",
+ "relay_10": "Relais 10",
+ "relay_11": "Relais 11",
+ "relay_12": "Relais 12",
+ "relay_13": "Relais 13",
+ "relay_14": "Relais 14",
+ "relay_15": "Relais 15",
+ "relay_16": "Relais 16",
+ "relay_2": "Relais 2",
+ "relay_3": "Relais 3",
+ "relay_4": "Relais 4",
+ "relay_5": "Relais 5",
+ "relay_6": "Relais 6",
+ "relay_7": "Relais 7",
+ "relay_8": "Relais 8",
+ "relay_9": "Relais 9"
+ },
+ "title": "Relais ariichten"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Board ariichten"
+ }
+ }
+ },
+ "title": "ProgettiHWSW Automatisme"
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/nl.json b/homeassistant/components/progettihwsw/translations/nl.json
new file mode 100644
index 00000000000000..2b30a4f1caac53
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/nl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "step": {
+ "relay_modes": {
+ "title": "Stel relais in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/pl.json b/homeassistant/components/progettihwsw/translations/pl.json
index ee25c598ecd2de..11a88b27477a0e 100644
--- a/homeassistant/components/progettihwsw/translations/pl.json
+++ b/homeassistant/components/progettihwsw/translations/pl.json
@@ -1,15 +1,43 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d",
+ "wrong_info_relay_modes": "Tryb przeka\u017anika musi by\u0107 monostabilny lub bistabilny."
},
"step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Przeka\u017anik 1",
+ "relay_10": "Przeka\u017anik 10",
+ "relay_11": "Przeka\u017anik 11",
+ "relay_12": "Przeka\u017anik 12",
+ "relay_13": "Przeka\u017anik 13",
+ "relay_14": "Przeka\u017anik 14",
+ "relay_15": "Przeka\u017anik 15",
+ "relay_16": "Przeka\u017anik 16",
+ "relay_2": "Przeka\u017anik 2",
+ "relay_3": "Przeka\u017anik 3",
+ "relay_4": "Przeka\u017anik 4",
+ "relay_5": "Przeka\u017anik 5",
+ "relay_6": "Przeka\u017anik 6",
+ "relay_7": "Przeka\u017anik 7",
+ "relay_8": "Przeka\u017anik 8",
+ "relay_9": "Przeka\u017anik 9"
+ },
+ "title": "Konfiguracja przeka\u017anik\u00f3w"
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"port": "Port"
- }
+ },
+ "title": "Konfiguracja uk\u0142adu"
}
}
- }
+ },
+ "title": "Automatyzacja ProgettiHWSW"
}
\ No newline at end of file
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index 87a8a2c41f3b13..bd9a6e35276df6 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -19,7 +19,9 @@
ATTR_MODE,
)
from homeassistant.const import (
+ ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
+ ATTR_FRIENDLY_NAME,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
CONTENT_TYPE_TEXT_PLAIN,
@@ -234,7 +236,7 @@ def _labels(state):
return {
"entity": state.entity_id,
"domain": state.domain,
- "friendly_name": state.attributes.get("friendly_name"),
+ "friendly_name": state.attributes.get(ATTR_FRIENDLY_NAME),
}
def _battery(self, state):
@@ -245,7 +247,7 @@ def _battery(self, state):
"Battery level as a percentage of its capacity",
)
try:
- value = float(state.attributes["battery_level"])
+ value = float(state.attributes[ATTR_BATTERY_LEVEL])
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py
index 7beaaaf00e19b3..2d0d14a69c1887 100644
--- a/homeassistant/components/proximity/__init__.py
+++ b/homeassistant/components/proximity/__init__.py
@@ -4,6 +4,8 @@
import voluptuous as vol
from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
CONF_DEVICES,
CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE,
@@ -149,8 +151,8 @@ def check_proximity_state_change(self, entity, old_state, new_state):
devices_in_zone = ""
zone_state = self.hass.states.get(self.proximity_zone)
- proximity_latitude = zone_state.attributes.get("latitude")
- proximity_longitude = zone_state.attributes.get("longitude")
+ proximity_latitude = zone_state.attributes.get(ATTR_LATITUDE)
+ proximity_longitude = zone_state.attributes.get(ATTR_LONGITUDE)
# Check for devices in the monitored zone.
for device in self.proximity_devices:
@@ -206,8 +208,8 @@ def check_proximity_state_change(self, entity, old_state, new_state):
dist_to_zone = distance(
proximity_latitude,
proximity_longitude,
- device_state.attributes["latitude"],
- device_state.attributes["longitude"],
+ device_state.attributes[ATTR_LATITUDE],
+ device_state.attributes[ATTR_LONGITUDE],
)
# Add the device and distance to a dictionary.
@@ -250,14 +252,14 @@ def check_proximity_state_change(self, entity, old_state, new_state):
old_distance = distance(
proximity_latitude,
proximity_longitude,
- old_state.attributes["latitude"],
- old_state.attributes["longitude"],
+ old_state.attributes[ATTR_LATITUDE],
+ old_state.attributes[ATTR_LONGITUDE],
)
new_distance = distance(
proximity_latitude,
proximity_longitude,
- new_state.attributes["latitude"],
- new_state.attributes["longitude"],
+ new_state.attributes[ATTR_LATITUDE],
+ new_state.attributes[ATTR_LONGITUDE],
)
distance_travelled = round(new_distance - old_distance, 1)
diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py
index 754f09fa19999c..c3f7151431a394 100644
--- a/homeassistant/components/proxy/camera.py
+++ b/homeassistant/components/proxy/camera.py
@@ -77,6 +77,8 @@ def _precheck_image(image, opts):
if imgfmt not in ("PNG", "JPEG"):
_LOGGER.warning("Image is of unsupported type: %s", imgfmt)
raise ValueError()
+ if not img.mode == "RGB":
+ img = img.convert("RGB")
return img
diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json
index c3a864565cf1e4..c538852db56b59 100644
--- a/homeassistant/components/ps4/strings.json
+++ b/homeassistant/components/ps4/strings.json
@@ -18,9 +18,9 @@
"description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"data": {
"region": "Region",
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"code": "PIN",
- "ip_address": "IP Address"
+ "ip_address": "[%key:common::config_flow::data::ip%]"
}
}
},
diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py
index 25b4531cee0ee6..a9b53c970bd49f 100644
--- a/homeassistant/components/pvpc_hourly_pricing/sensor.py
+++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py
@@ -6,7 +6,7 @@
from aiopvpc import PVPCData
from homeassistant import config_entries
-from homeassistant.const import CONF_NAME, ENERGY_KILO_WATT_HOUR
+from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_call_later, async_track_time_change
@@ -19,7 +19,7 @@
ATTR_PRICE = "price"
ICON = "mdi:currency-eur"
-UNIT = f"€/{ENERGY_KILO_WATT_HOUR}"
+UNIT = f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
_DEFAULT_TIMEOUT = 10
diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json
index fbda22c51495d0..a1536d2186f301 100644
--- a/homeassistant/components/pvpc_hourly_pricing/strings.json
+++ b/homeassistant/components/pvpc_hourly_pricing/strings.json
@@ -11,7 +11,7 @@
}
},
"abort": {
- "already_configured": "Integration is already configured with an existing sensor with that tariff"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
index 8eeab499620e48..bc5f3a59428440 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Integraci\u00f3 ja configurada amb un sensor amb aquesta tarifa"
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json
index 7716dab8ad7cd1..02acb46eeb62b9 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Integration is already configured with an existing sensor with that tariff"
+ "already_configured": "Service is already configured"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json
index 4048e3577517a2..5386529e43a185 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json
@@ -9,6 +9,7 @@
"name": "Nom du capteur",
"tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)"
},
+ "description": "Ce capteur utilise l'API officielle pour obtenir la [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espagne. \n Pour une explication plus pr\u00e9cise, visitez la [documentation d'int\u00e9gration] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n S\u00e9lectionnez le tarif contract\u00e9 en fonction du nombre de p\u00e9riodes de facturation par jour: \n - 1 p\u00e9riode: normale \n - 2 p\u00e9riodes: discrimination (tarif \u00e0 la nuit) \n - 3 p\u00e9riodes: voiture \u00e9lectrique (tarif \u00e0 la nuit sur 3 p\u00e9riodes)",
"title": "S\u00e9lection tarifaire"
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json
index 84f2fddcb8331a..e36fc74688304c 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con quella tariffa"
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json
index d27b78615cf4d2..7eec443fcaa416 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Integrasjon er allerede konfigurert med en eksisterende sensor med den tariffen"
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
index 4a2f4440a2a0df..e48e5222f3c6fe 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json
index faea255693e9ff..c6f1f4ca2647b8 100644
--- a/homeassistant/components/rachio/strings.json
+++ b/homeassistant/components/rachio/strings.json
@@ -10,12 +10,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py
index 32ec6267941ffb..b5dc71b585c8d0 100644
--- a/homeassistant/components/rachio/switch.py
+++ b/homeassistant/components/rachio/switch.py
@@ -56,6 +56,7 @@
SUBTYPE_SLEEP_MODE_OFF,
SUBTYPE_SLEEP_MODE_ON,
SUBTYPE_ZONE_COMPLETED,
+ SUBTYPE_ZONE_PAUSED,
SUBTYPE_ZONE_STARTED,
SUBTYPE_ZONE_STOPPED,
)
@@ -392,7 +393,11 @@ def _async_handle_update(self, *args, **kwargs) -> None:
if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED:
self._state = True
- elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, SUBTYPE_ZONE_COMPLETED]:
+ elif args[0][KEY_SUBTYPE] in [
+ SUBTYPE_ZONE_STOPPED,
+ SUBTYPE_ZONE_COMPLETED,
+ SUBTYPE_ZONE_PAUSED,
+ ]:
self._state = False
self.async_write_ha_state()
diff --git a/homeassistant/components/rachio/translations/ca.json b/homeassistant/components/rachio/translations/ca.json
index 8da88ec50e92fc..c451b4df83eb8a 100644
--- a/homeassistant/components/rachio/translations/ca.json
+++ b/homeassistant/components/rachio/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/rachio/translations/en.json b/homeassistant/components/rachio/translations/en.json
index 21f7524008a28d..ca466938a3c23e 100644
--- a/homeassistant/components/rachio/translations/en.json
+++ b/homeassistant/components/rachio/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/rachio/translations/it.json b/homeassistant/components/rachio/translations/it.json
index f8013eb3551aea..49d64be152339d 100644
--- a/homeassistant/components/rachio/translations/it.json
+++ b/homeassistant/components/rachio/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/rachio/translations/no.json b/homeassistant/components/rachio/translations/no.json
index e5f9c0abbd3f7b..7d0c4feae6c836 100644
--- a/homeassistant/components/rachio/translations/no.json
+++ b/homeassistant/components/rachio/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/rachio/translations/pl.json b/homeassistant/components/rachio/translations/pl.json
index e077fea03a4412..55e8d18fe1c1c9 100644
--- a/homeassistant/components/rachio/translations/pl.json
+++ b/homeassistant/components/rachio/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/rachio/translations/ru.json b/homeassistant/components/rachio/translations/ru.json
index f6fe53b5f8dc5a..c85a07e58bf3cf 100644
--- a/homeassistant/components/rachio/translations/ru.json
+++ b/homeassistant/components/rachio/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json
index 7f05aa218e715f..c79264706c6919 100644
--- a/homeassistant/components/rachio/translations/zh-Hant.json
+++ b/homeassistant/components/rachio/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py
index 5daf78527255e9..c175117efcb344 100644
--- a/homeassistant/components/rachio/webhooks.py
+++ b/homeassistant/components/rachio/webhooks.py
@@ -58,6 +58,7 @@
SUBTYPE_ZONE_COMPLETED = "ZONE_COMPLETED"
SUBTYPE_ZONE_CYCLING = "ZONE_CYCLING"
SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED"
+SUBTYPE_ZONE_PAUSED = "ZONE_PAUSED"
# Webhook callbacks
LISTEN_EVENT_TYPES = [
diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py
index d0513ac89fb232..b6e429f2e83c7b 100644
--- a/homeassistant/components/rainmachine/config_flow.py
+++ b/homeassistant/components/rainmachine/config_flow.py
@@ -69,7 +69,7 @@ async def async_step_user(self, user_input=None):
ssl=user_input.get(CONF_SSL, True),
)
except RainMachineError:
- return await self._show_form({CONF_PASSWORD: "invalid_credentials"})
+ return await self._show_form({CONF_PASSWORD: "invalid_auth"})
# Unfortunately, RainMachine doesn't provide a way to refresh the
# access token without using the IP address and password, so we have to
diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py
index 6f87c34d60765b..8534748978d68c 100644
--- a/homeassistant/components/rainmachine/sensor.py
+++ b/homeassistant/components/rainmachine/sensor.py
@@ -1,7 +1,7 @@
"""This platform provides support for sensor data from RainMachine."""
import logging
-from homeassistant.const import TEMP_CELSIUS
+from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -26,7 +26,7 @@
TYPE_FLOW_SENSOR_CLICK_M3: (
"Flow Sensor Clicks",
"mdi:water-pump",
- "clicks/m^3",
+ f"clicks/{VOLUME_CUBIC_METERS}",
None,
False,
DATA_PROVISION_SETTINGS,
diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json
index 555230d1f0f9d9..d0a6adf468736c 100644
--- a/homeassistant/components/rainmachine/strings.json
+++ b/homeassistant/components/rainmachine/strings.json
@@ -11,11 +11,10 @@
}
},
"error": {
- "identifier_exists": "Account already registered",
- "invalid_credentials": "Invalid credentials"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "This RainMachine controller is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json
index 37d842355c0fa4..cc77b66aef05eb 100644
--- a/homeassistant/components/rainmachine/translations/ca.json
+++ b/homeassistant/components/rainmachine/translations/ca.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Aquest controlador RainMachine ja est\u00e0 configurat."
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "identifier_exists": "Aquest compte ja est\u00e0 registrat",
+ "identifier_exists": "El compte ja ha estat configurat",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Credencials inv\u00e0lides"
},
"step": {
diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json
index aabe3d352935b3..d734cbd5d388e1 100644
--- a/homeassistant/components/rainmachine/translations/en.json
+++ b/homeassistant/components/rainmachine/translations/en.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "This RainMachine controller is already configured."
+ "already_configured": "Device is already configured"
},
"error": {
- "identifier_exists": "Account already registered",
+ "identifier_exists": "Account is already configured",
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid credentials"
},
"step": {
diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json
index 0767c509bf9bdc..d788b68dcbb41b 100644
--- a/homeassistant/components/rainmachine/translations/es.json
+++ b/homeassistant/components/rainmachine/translations/es.json
@@ -5,6 +5,7 @@
},
"error": {
"identifier_exists": "Cuenta ya registrada",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Credenciales no v\u00e1lidas"
},
"step": {
diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json
new file mode 100644
index 00000000000000..2227b7442a79c6
--- /dev/null
+++ b/homeassistant/components/rainmachine/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json
index 5ec522c9c24aed..7850ea19c33e61 100644
--- a/homeassistant/components/rainmachine/translations/it.json
+++ b/homeassistant/components/rainmachine/translations/it.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Questo controller RainMachine \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "identifier_exists": "Account gi\u00e0 registrato",
+ "identifier_exists": "L'account \u00e8 gi\u00e0 configurato",
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Credenziali non valide"
},
"step": {
diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json
index cd5e3d85d50c4d..3bb1a95f647457 100644
--- a/homeassistant/components/rainmachine/translations/ru.json
+++ b/homeassistant/components/rainmachine/translations/ru.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
- "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
+ "identifier_exists": "\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.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
},
"step": {
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 9ce950fedfe265..8e6a251f660d47 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -196,6 +196,10 @@ async def async_handle_purge_service(service):
PurgeTask = namedtuple("PurgeTask", ["keep_days", "repack"])
+class WaitTask:
+ """An object to insert into the recorder queue to tell it set the _queue_watch event."""
+
+
class Recorder(threading.Thread):
"""A threaded recorder class."""
@@ -226,6 +230,7 @@ def __init__(
self.db_retry_wait = db_retry_wait
self.db_integrity_check = db_integrity_check
self.async_db_ready = asyncio.Future()
+ self._queue_watch = threading.Event()
self.engine: Any = None
self.run_info: Any = None
@@ -234,7 +239,8 @@ def __init__(
self._timechanges_seen = 0
self._keepalive_count = 0
- self._old_state_ids = {}
+ self._old_states = {}
+ self._pending_expunge = []
self.event_session = None
self.get_session = None
self._completed_database_setup = False
@@ -353,6 +359,9 @@ def async_purge(now):
if not purge.purge_old_data(self, event.keep_days, event.repack):
self.queue.put(PurgeTask(event.keep_days, event.repack))
continue
+ if isinstance(event, WaitTask):
+ self._queue_watch.set()
+ continue
if event.event_type == EVENT_TIME_CHANGED:
self._keepalive_count += 1
if self._keepalive_count >= KEEPALIVE_TIME:
@@ -373,11 +382,11 @@ def async_purge(now):
continue
try:
- dbevent = Events.from_event(event)
if event.event_type == EVENT_STATE_CHANGED:
- dbevent.event_data = "{}"
+ dbevent = Events.from_event(event, event_data="{}")
+ else:
+ dbevent = Events.from_event(event)
self.event_session.add(dbevent)
- self.event_session.flush()
except (TypeError, ValueError):
_LOGGER.warning("Event is not JSON serializable: %s", event)
except Exception as err: # pylint: disable=broad-except
@@ -388,16 +397,15 @@ def async_purge(now):
try:
dbstate = States.from_event(event)
has_new_state = event.data.get("new_state")
- dbstate.old_state_id = self._old_state_ids.get(dbstate.entity_id)
+ if dbstate.entity_id in self._old_states:
+ dbstate.old_state = self._old_states.pop(dbstate.entity_id)
if not has_new_state:
dbstate.state = None
- dbstate.event_id = dbevent.event_id
+ dbstate.event = dbevent
self.event_session.add(dbstate)
- self.event_session.flush()
if has_new_state:
- self._old_state_ids[dbstate.entity_id] = dbstate.state_id
- elif dbstate.entity_id in self._old_state_ids:
- del self._old_state_ids[dbstate.entity_id]
+ self._old_states[dbstate.entity_id] = dbstate
+ self._pending_expunge.append(dbstate)
except (TypeError, ValueError):
_LOGGER.warning(
"State is not JSON serializable: %s",
@@ -483,6 +491,12 @@ def _reopen_event_session(self):
def _commit_event_session(self):
try:
+ self.event_session.flush()
+ for dbstate in self._pending_expunge:
+ # Expunge the state so its not expired
+ # until we use it later for dbstate.old_state
+ self.event_session.expunge(dbstate)
+ self._pending_expunge = []
self.event_session.commit()
except Exception as err:
_LOGGER.error("Error executing query: %s", err)
@@ -506,8 +520,9 @@ def block_till_done(self):
after calling this to ensure the data
is in the database.
"""
- while not self.queue.empty():
- time.sleep(0.025)
+ self._queue_watch.clear()
+ self.queue.put(WaitTask())
+ self._queue_watch.wait()
def _setup_connection(self):
"""Ensure database is ready to fly."""
diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py
index b5384cf84cbfaa..4756ac13ce3001 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -14,6 +14,7 @@
distinct,
)
from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relationship
from sqlalchemy.orm.session import Session
from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id
@@ -59,12 +60,12 @@ class Events(Base): # type: ignore
)
@staticmethod
- def from_event(event):
+ def from_event(event, event_data=None):
"""Create an event database object from a native event."""
return Events(
event_type=event.event_type,
- event_data=json.dumps(event.data, cls=JSONEncoder),
- origin=str(event.origin),
+ event_data=event_data or json.dumps(event.data, cls=JSONEncoder),
+ origin=str(event.origin.value),
time_fired=event.time_fired,
context_id=event.context.id,
context_user_id=event.context.user_id,
@@ -105,7 +106,9 @@ class States(Base): # type: ignore
last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True)
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
- old_state_id = Column(Integer)
+ old_state_id = Column(Integer, ForeignKey("states.state_id"))
+ event = relationship("Events", uselist=False)
+ old_state = relationship("States", remote_side=[state_id])
__table_args__ = (
# Used for fetching the state of entities at a specific time
@@ -218,7 +221,8 @@ def process_timestamp_to_utc_isoformat(ts):
"""Process a timestamp into UTC isotime."""
if ts is None:
return None
+ if ts.tzinfo == dt_util.UTC:
+ return ts.isoformat()
if ts.tzinfo is None:
return f"{ts.isoformat()}{DB_TIMEZONE}"
-
- return dt_util.as_utc(ts).isoformat()
+ return ts.astimezone(dt_util.UTC).isoformat()
diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json
index 82ba4812592777..6f91e2a9abe187 100644
--- a/homeassistant/components/rejseplanen/manifest.json
+++ b/homeassistant/components/rejseplanen/manifest.json
@@ -3,5 +3,5 @@
"name": "Rejseplanen",
"documentation": "https://www.home-assistant.io/integrations/rejseplanen",
"requirements": ["rjpl==0.3.6"],
- "codeowners": []
+ "codeowners": ["@DarkFox"]
}
diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/remote/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/remote/translations/de.json b/homeassistant/components/remote/translations/de.json
index d1ec188e2b89c0..ffd542f27d935d 100644
--- a/homeassistant/components/remote/translations/de.json
+++ b/homeassistant/components/remote/translations/de.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} umschalten",
+ "turn_off": "Schalte {entity_name} aus",
+ "turn_on": "Schalte {entity_name} an"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ist ausgeschaltet",
+ "is_on": "{entity_name} ist eingeschaltet"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} ausgeschaltet",
+ "turned_on": "{entity_name} eingeschaltet"
+ }
+ },
"state": {
"_": {
"off": "Aus",
diff --git a/homeassistant/components/remote/translations/el.json b/homeassistant/components/remote/translations/el.json
index 79860300b96b37..0ef87433fce380 100644
--- a/homeassistant/components/remote/translations/el.json
+++ b/homeassistant/components/remote/translations/el.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}",
+ "turn_off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}",
+ "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf",
+ "is_on": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5",
+ "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5"
+ }
+ },
"state": {
"_": {
"off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc",
diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json
index bf8b6d3a3eccdc..e2452b80e89cd1 100644
--- a/homeassistant/components/remote/translations/es.json
+++ b/homeassistant/components/remote/translations/es.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Alternar {entity_name}",
+ "turn_off": "Apagar {entity_name}",
+ "turn_on": "Encender {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e1 apagado",
+ "is_on": "{entity_name} est\u00e1 activado"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} desactivado",
+ "turned_on": "{entity_name} activado"
+ }
+ },
"state": {
"_": {
"off": "Apagado",
diff --git a/homeassistant/components/remote/translations/et.json b/homeassistant/components/remote/translations/et.json
index 6bcfbf7f4cfef4..458704b9999934 100644
--- a/homeassistant/components/remote/translations/et.json
+++ b/homeassistant/components/remote/translations/et.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Muuda {entity_name} olekut",
+ "turn_off": "L\u00fclita {entity_name} v\u00e4lja",
+ "turn_on": "L\u00fclita {entity_name} sisse"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
diff --git a/homeassistant/components/remote/translations/ko.json b/homeassistant/components/remote/translations/ko.json
index b866fd7fee56d1..bd055e21f5bb73 100644
--- a/homeassistant/components/remote/translations/ko.json
+++ b/homeassistant/components/remote/translations/ko.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \ud1a0\uae00",
+ "turn_off": "{entity_name} \ub044\uae30",
+ "turn_on": "{entity_name} \ucf1c\uae30"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \uc774 \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name} \uc774 \ucf1c\uc838 \uc788\uc73c\uba74"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \uaebc\uc9d0",
+ "turned_on": "{entity_name} \ucf1c\uc9d0"
+ }
+ },
"state": {
"_": {
"off": "\uaebc\uc9d0",
diff --git a/homeassistant/components/remote/translations/lb.json b/homeassistant/components/remote/translations/lb.json
index b81e82470fc7fb..f9f81a85a2992b 100644
--- a/homeassistant/components/remote/translations/lb.json
+++ b/homeassistant/components/remote/translations/lb.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \u00ebmschalten",
+ "turn_off": "{entity_name} ausschalten",
+ "turn_on": "{entity_name} uschalten"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ass ausgeschalt",
+ "is_on": "{entity_name} ass un"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} gouf ausgeschalt",
+ "turned_on": "{entity_name} gouf ugeschalt"
+ }
+ },
"state": {
"_": {
"off": "Aus",
diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json
index b3ccad9ae2b5ab..18d984f5c68f5a 100644
--- a/homeassistant/components/remote/translations/nl.json
+++ b/homeassistant/components/remote/translations/nl.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Schakel {entity_name}",
+ "turn_off": "{entity_name} uitschakelen",
+ "turn_on": "{entity_name} inschakelen"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} staat uit",
+ "is_on": "{entity_name} staat aan"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} uitgeschakeld",
+ "turned_on": "{entity_name} ingeschakeld"
+ }
+ },
"state": {
"_": {
"off": "Uit",
diff --git a/homeassistant/components/remote/translations/pl.json b/homeassistant/components/remote/translations/pl.json
index e6621d8b42dd96..e36c438b85b967 100644
--- a/homeassistant/components/remote/translations/pl.json
+++ b/homeassistant/components/remote/translations/pl.json
@@ -1,4 +1,15 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Prze\u0142\u0105cz {entity_name}",
+ "turn_off": "Wy\u0142\u0105cz {entity_name}",
+ "turn_on": "W\u0142\u0105cz {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} jest wy\u0142\u0105czony",
+ "is_on": "{entity_name} jest w\u0142\u0105czony"
+ }
+ },
"state": {
"_": {
"off": "wy\u0142\u0105czony",
diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json
index ea82df41e758b3..1b6584c5bf8694 100644
--- a/homeassistant/components/remote/translations/sv.json
+++ b/homeassistant/components/remote/translations/sv.json
@@ -1,4 +1,18 @@
{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "St\u00e4ng av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e4r avst\u00e4ngd",
+ "is_on": "{entity_name} \u00e4r p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} st\u00e4ngdes av",
+ "turned_on": "{entity_name} slogs p\u00e5"
+ }
+ },
"state": {
"_": {
"off": "Av",
diff --git a/homeassistant/components/remote/translations/uk.json b/homeassistant/components/remote/translations/uk.json
index bc52ed67ae5c0a..2feda4928e5847 100644
--- a/homeassistant/components/remote/translations/uk.json
+++ b/homeassistant/components/remote/translations/uk.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py
index 43b5c8bd7e89c6..b1996bdce50fdd 100644
--- a/homeassistant/components/rest_command/__init__.py
+++ b/homeassistant/components/rest_command/__init__.py
@@ -136,10 +136,10 @@ async def async_service_handler(service):
)
except asyncio.TimeoutError:
- _LOGGER.warning("Timeout call %s", request_url, exc_info=1)
+ _LOGGER.warning("Timeout call %s", request_url)
except aiohttp.ClientError:
- _LOGGER.error("Client error %s", request_url, exc_info=1)
+ _LOGGER.error("Client error %s", request_url)
# register services
hass.services.async_register(DOMAIN, name, async_service_handler)
diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py
index 13e3a0f65da84b..6d63e12378d2db 100644
--- a/homeassistant/components/rflink/light.py
+++ b/homeassistant/components/rflink/light.py
@@ -197,10 +197,9 @@ def brightness(self):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attr = {}
- if self._brightness is not None:
- attr[ATTR_BRIGHTNESS] = self._brightness
- return attr
+ if self._brightness is None:
+ return {}
+ return {ATTR_BRIGHTNESS: self._brightness}
@property
def supported_features(self):
@@ -260,10 +259,9 @@ def brightness(self):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attr = {}
- if self._brightness is not None:
- attr[ATTR_BRIGHTNESS] = self._brightness
- return attr
+ if self._brightness is None:
+ return {}
+ return {ATTR_BRIGHTNESS: self._brightness}
@property
def supported_features(self):
diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py
index 6082d54df12d8b..22cc3ea4c9a3ef 100644
--- a/homeassistant/components/rfxtrx/__init__.py
+++ b/homeassistant/components/rfxtrx/__init__.py
@@ -5,6 +5,7 @@
import logging
import RFXtrx as rfxtrxmod
+import async_timeout
import voluptuous as vol
from homeassistant import config_entries
@@ -18,18 +19,35 @@
CONF_DEVICES,
CONF_HOST,
CONF_PORT,
+ DEGREE,
+ ELECTRICAL_CURRENT_AMPERE,
+ ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_STOP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ SPEED_METERS_PER_SECOND,
TEMP_CELSIUS,
+ TIME_HOURS,
UV_INDEX,
+ VOLT,
)
from homeassistant.core import callback
+from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
ATTR_EVENT,
+ CONF_AUTOMATIC_ADD,
+ CONF_DATA_BITS,
+ CONF_DEBUG,
+ CONF_FIRE_EVENT,
+ CONF_OFF_DELAY,
+ CONF_REMOVE_DEVICE,
+ CONF_SIGNAL_REPETITIONS,
DEVICE_PACKET_TYPE_LIGHTING4,
EVENT_RFXTRX_EVENT,
SERVICE_SEND,
@@ -39,12 +57,6 @@
DEFAULT_SIGNAL_REPETITIONS = 1
-CONF_FIRE_EVENT = "fire_event"
-CONF_DATA_BITS = "data_bits"
-CONF_AUTOMATIC_ADD = "automatic_add"
-CONF_SIGNAL_REPETITIONS = "signal_repetitions"
-CONF_DEBUG = "debug"
-CONF_OFF_DELAY = "off_delay"
SIGNAL_EVENT = f"{DOMAIN}_event"
DATA_TYPES = OrderedDict(
@@ -52,32 +64,30 @@
("Temperature", TEMP_CELSIUS),
("Temperature2", TEMP_CELSIUS),
("Humidity", PERCENTAGE),
- ("Barometer", ""),
- ("Wind direction", ""),
- ("Rain rate", ""),
+ ("Barometer", PRESSURE_HPA),
+ ("Wind direction", DEGREE),
+ ("Rain rate", f"{LENGTH_MILLIMETERS}/{TIME_HOURS}"),
("Energy usage", POWER_WATT),
- ("Total usage", POWER_WATT),
- ("Sound", ""),
- ("Sensor Status", ""),
- ("Counter value", ""),
+ ("Total usage", ENERGY_KILO_WATT_HOUR),
+ ("Sound", None),
+ ("Sensor Status", None),
+ ("Counter value", "count"),
("UV", UV_INDEX),
- ("Humidity status", ""),
- ("Forecast", ""),
- ("Forecast numeric", ""),
- ("Rain total", ""),
- ("Wind average speed", ""),
- ("Wind gust", ""),
- ("Chill", ""),
- ("Total usage", ""),
- ("Count", ""),
- ("Current Ch. 1", ""),
- ("Current Ch. 2", ""),
- ("Current Ch. 3", ""),
- ("Energy usage", ""),
- ("Voltage", ""),
- ("Current", ""),
+ ("Humidity status", None),
+ ("Forecast", None),
+ ("Forecast numeric", None),
+ ("Rain total", LENGTH_MILLIMETERS),
+ ("Wind average speed", SPEED_METERS_PER_SECOND),
+ ("Wind gust", SPEED_METERS_PER_SECOND),
+ ("Chill", TEMP_CELSIUS),
+ ("Count", "count"),
+ ("Current Ch. 1", ELECTRICAL_CURRENT_AMPERE),
+ ("Current Ch. 2", ELECTRICAL_CURRENT_AMPERE),
+ ("Current Ch. 3", ELECTRICAL_CURRENT_AMPERE),
+ ("Voltage", VOLT),
+ ("Current", ELECTRICAL_CURRENT_AMPERE),
("Battery numeric", PERCENTAGE),
- ("Rssi numeric", "dBm"),
+ ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT),
]
)
@@ -120,10 +130,10 @@ def _ensure_device(value):
BASE_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_DEBUG, default=False): cv.boolean,
+ vol.Optional(CONF_DEBUG): cv.boolean,
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
vol.Optional(CONF_DEVICES, default={}): {cv.string: _ensure_device},
- }
+ },
)
DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string})
@@ -133,7 +143,8 @@ def _ensure_device(value):
)
CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA
+ {DOMAIN: vol.All(cv.deprecated(CONF_DEBUG), vol.Any(DEVICE_SCHEMA, PORT_SCHEMA))},
+ extra=vol.ALLOW_EXTRA,
)
DOMAINS = ["switch", "sensor", "light", "binary_sensor", "cover"]
@@ -148,7 +159,6 @@ async def async_setup(hass, config):
CONF_HOST: config[DOMAIN].get(CONF_HOST),
CONF_PORT: config[DOMAIN].get(CONF_PORT),
CONF_DEVICE: config[DOMAIN].get(CONF_DEVICE),
- CONF_DEBUG: config[DOMAIN].get(CONF_DEBUG),
CONF_AUTOMATIC_ADD: config[DOMAIN].get(CONF_AUTOMATIC_ADD),
CONF_DEVICES: config[DOMAIN][CONF_DEVICES],
}
@@ -217,18 +227,17 @@ def _create_rfx(config):
rfx = rfxtrxmod.Connect(
(config[CONF_HOST], config[CONF_PORT]),
None,
- debug=config[CONF_DEBUG],
transport_protocol=rfxtrxmod.PyNetworkTransport,
)
else:
- rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None, debug=config[CONF_DEBUG])
+ rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None)
return rfx
def _get_device_lookup(devices):
"""Get a lookup structure for devices."""
- lookup = dict()
+ lookup = {}
for event_code, event_config in devices.items():
event = get_rfx_object(event_code)
if event is None:
@@ -245,7 +254,11 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry):
config = entry.data
# Initialize library
- rfx_object = await hass.async_add_executor_job(_create_rfx, config)
+ try:
+ async with async_timeout.timeout(5):
+ rfx_object = await hass.async_add_executor_job(_create_rfx, config)
+ except asyncio.TimeoutError as err:
+ raise ConfigEntryNotReady from err
# Setup some per device config
devices = _get_device_lookup(config[CONF_DEVICES])
@@ -438,6 +451,12 @@ async def async_added_to_hass(self):
)
)
+ self.async_on_remove(
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove
+ )
+ )
+
@property
def should_poll(self):
"""No polling needed for a RFXtrx switch."""
diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py
index 21f3e0b74b31e8..7fe89e747bcdfb 100644
--- a/homeassistant/components/rfxtrx/binary_sensor.py
+++ b/homeassistant/components/rfxtrx/binary_sensor.py
@@ -61,6 +61,18 @@
}
+def supported(event):
+ """Return whether an event supports binary_sensor."""
+ if isinstance(event, rfxtrxmod.ControlEvent):
+ return True
+ if isinstance(event, rfxtrxmod.SensorEvent):
+ return event.values.get("Sensor Status") in [
+ *SENSOR_STATUS_ON,
+ *SENSOR_STATUS_OFF,
+ ]
+ return False
+
+
async def async_setup_entry(
hass,
config_entry,
@@ -74,16 +86,6 @@ async def async_setup_entry(
discovery_info = config_entry.data
- def supported(event):
- if isinstance(event, rfxtrxmod.ControlEvent):
- return True
- if isinstance(event, rfxtrxmod.SensorEvent):
- return event.values.get("Sensor Status") in [
- *SENSOR_STATUS_ON,
- *SENSOR_STATUS_OFF,
- ]
- return False
-
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
if event is None:
diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py
index 287e1ec4baf6d1..db7ca49691a939 100644
--- a/homeassistant/components/rfxtrx/config_flow.py
+++ b/homeassistant/components/rfxtrx/config_flow.py
@@ -1,12 +1,404 @@
"""Config flow for RFXCOM RFXtrx integration."""
+import copy
import logging
+import os
-from homeassistant import config_entries
+import RFXtrx as rfxtrxmod
+import serial
+import serial.tools.list_ports
+import voluptuous as vol
-from . import DOMAIN
+from homeassistant import config_entries, exceptions
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_COMMAND_OFF,
+ CONF_COMMAND_ON,
+ CONF_DEVICE,
+ CONF_DEVICE_ID,
+ CONF_DEVICES,
+ CONF_HOST,
+ CONF_PORT,
+ CONF_TYPE,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.device_registry import (
+ async_entries_for_config_entry,
+ async_get_registry as async_get_device_registry,
+)
+from homeassistant.helpers.entity_registry import (
+ async_entries_for_device,
+ async_get_registry as async_get_entity_registry,
+)
+
+from . import DOMAIN, get_device_id, get_rfx_object
+from .binary_sensor import supported as binary_supported
+from .const import (
+ CONF_AUTOMATIC_ADD,
+ CONF_DATA_BITS,
+ CONF_FIRE_EVENT,
+ CONF_OFF_DELAY,
+ CONF_REMOVE_DEVICE,
+ CONF_REPLACE_DEVICE,
+ CONF_SIGNAL_REPETITIONS,
+ DEVICE_PACKET_TYPE_LIGHTING4,
+)
+from .cover import supported as cover_supported
+from .light import supported as light_supported
+from .switch import supported as switch_supported
_LOGGER = logging.getLogger(__name__)
+CONF_EVENT_CODE = "event_code"
+CONF_MANUAL_PATH = "Enter Manually"
+
+
+def none_or_int(value, base):
+ """Check if strin is one otherwise convert to int."""
+ if value is None:
+ return None
+ return int(value, base)
+
+
+class OptionsFlow(config_entries.OptionsFlow):
+ """Handle Rfxtrx options."""
+
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize rfxtrx options flow."""
+ self._config_entry = config_entry
+ self._global_options = None
+ self._selected_device = None
+ self._selected_device_entry_id = None
+ self._selected_device_event_code = None
+ self._selected_device_object = None
+ self._device_entries = None
+ self._device_registry = None
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ return await self.async_step_prompt_options()
+
+ async def async_step_prompt_options(self, user_input=None):
+ """Prompt for options."""
+ errors = {}
+
+ if user_input is not None:
+ self._global_options = {
+ CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD],
+ }
+ if CONF_DEVICE in user_input:
+ entry_id = user_input[CONF_DEVICE]
+ device_data = self._get_device_data(entry_id)
+ self._selected_device_entry_id = entry_id
+ event_code = device_data[CONF_EVENT_CODE]
+ self._selected_device_event_code = event_code
+ self._selected_device = self._config_entry.data[CONF_DEVICES][
+ event_code
+ ]
+ self._selected_device_object = get_rfx_object(event_code)
+ return await self.async_step_set_device_options()
+ if CONF_REMOVE_DEVICE in user_input:
+ remove_devices = user_input[CONF_REMOVE_DEVICE]
+ devices = {}
+ for entry_id in remove_devices:
+ device_data = self._get_device_data(entry_id)
+
+ event_code = device_data[CONF_EVENT_CODE]
+ device_id = device_data[CONF_DEVICE_ID]
+ self.hass.helpers.dispatcher.async_dispatcher_send(
+ f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{device_id}"
+ )
+ self._device_registry.async_remove_device(entry_id)
+ devices[event_code] = None
+
+ self.update_config_data(
+ global_options=self._global_options, devices=devices
+ )
+
+ return self.async_create_entry(title="", data={})
+ if CONF_EVENT_CODE in user_input:
+ self._selected_device_event_code = user_input[CONF_EVENT_CODE]
+ self._selected_device = {}
+ selected_device_object = get_rfx_object(
+ self._selected_device_event_code
+ )
+ if selected_device_object is None:
+ errors[CONF_EVENT_CODE] = "invalid_event_code"
+ elif not self._can_add_device(selected_device_object):
+ errors[CONF_EVENT_CODE] = "already_configured_device"
+ else:
+ self._selected_device_object = selected_device_object
+ return await self.async_step_set_device_options()
+
+ if not errors:
+ self.update_config_data(global_options=self._global_options)
+
+ return self.async_create_entry(title="", data={})
+
+ device_registry = await async_get_device_registry(self.hass)
+ device_entries = async_entries_for_config_entry(
+ device_registry, self._config_entry.entry_id
+ )
+ self._device_registry = device_registry
+ self._device_entries = device_entries
+
+ devices = {
+ entry.id: entry.name_by_user if entry.name_by_user else entry.name
+ for entry in device_entries
+ }
+
+ options = {
+ vol.Optional(
+ CONF_AUTOMATIC_ADD,
+ default=self._config_entry.data[CONF_AUTOMATIC_ADD],
+ ): bool,
+ vol.Optional(CONF_EVENT_CODE): str,
+ vol.Optional(CONF_DEVICE): vol.In(devices),
+ vol.Optional(CONF_REMOVE_DEVICE): cv.multi_select(devices),
+ }
+
+ return self.async_show_form(
+ step_id="prompt_options", data_schema=vol.Schema(options), errors=errors
+ )
+
+ async def async_step_set_device_options(self, user_input=None):
+ """Manage device options."""
+ errors = {}
+
+ if user_input is not None:
+ device_id = get_device_id(
+ self._selected_device_object.device,
+ data_bits=user_input.get(CONF_DATA_BITS),
+ )
+
+ if CONF_REPLACE_DEVICE in user_input:
+ await self._async_replace_device(user_input[CONF_REPLACE_DEVICE])
+
+ devices = {self._selected_device_event_code: None}
+ self.update_config_data(
+ global_options=self._global_options, devices=devices
+ )
+
+ return self.async_create_entry(title="", data={})
+
+ try:
+ command_on = none_or_int(user_input.get(CONF_COMMAND_ON), 16)
+ except ValueError:
+ errors[CONF_COMMAND_ON] = "invalid_input_2262_on"
+
+ try:
+ command_off = none_or_int(user_input.get(CONF_COMMAND_OFF), 16)
+ except ValueError:
+ errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
+
+ try:
+ off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10)
+ except ValueError:
+ errors[CONF_OFF_DELAY] = "invalid_input_off_delay"
+
+ if not errors:
+ devices = {}
+ device = {
+ CONF_DEVICE_ID: device_id,
+ CONF_FIRE_EVENT: user_input.get(CONF_FIRE_EVENT, False),
+ CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1),
+ }
+
+ devices[self._selected_device_event_code] = device
+
+ if off_delay:
+ device[CONF_OFF_DELAY] = off_delay
+ if user_input.get(CONF_DATA_BITS):
+ device[CONF_DATA_BITS] = user_input[CONF_DATA_BITS]
+ if command_on:
+ device[CONF_COMMAND_ON] = command_on
+ if command_off:
+ device[CONF_COMMAND_OFF] = command_off
+
+ self.update_config_data(
+ global_options=self._global_options, devices=devices
+ )
+
+ return self.async_create_entry(title="", data={})
+
+ device_data = self._selected_device
+
+ data_schema = {
+ vol.Optional(
+ CONF_FIRE_EVENT, default=device_data.get(CONF_FIRE_EVENT, False)
+ ): bool,
+ }
+
+ if binary_supported(self._selected_device_object):
+ if device_data.get(CONF_OFF_DELAY):
+ off_delay_schema = {
+ vol.Optional(
+ CONF_OFF_DELAY,
+ description={"suggested_value": device_data[CONF_OFF_DELAY]},
+ ): str,
+ }
+ else:
+ off_delay_schema = {
+ vol.Optional(CONF_OFF_DELAY): str,
+ }
+ data_schema.update(off_delay_schema)
+
+ if (
+ binary_supported(self._selected_device_object)
+ or cover_supported(self._selected_device_object)
+ or light_supported(self._selected_device_object)
+ or switch_supported(self._selected_device_object)
+ ):
+ data_schema.update(
+ {
+ vol.Optional(
+ CONF_SIGNAL_REPETITIONS,
+ default=device_data.get(CONF_SIGNAL_REPETITIONS, 1),
+ ): int,
+ }
+ )
+
+ if (
+ self._selected_device_object.device.packettype
+ == DEVICE_PACKET_TYPE_LIGHTING4
+ ):
+ data_schema.update(
+ {
+ vol.Optional(
+ CONF_DATA_BITS, default=device_data.get(CONF_DATA_BITS, 0)
+ ): int,
+ vol.Optional(
+ CONF_COMMAND_ON,
+ default=hex(device_data.get(CONF_COMMAND_ON, 0)),
+ ): str,
+ vol.Optional(
+ CONF_COMMAND_OFF,
+ default=hex(device_data.get(CONF_COMMAND_OFF, 0)),
+ ): str,
+ }
+ )
+
+ devices = {
+ entry.id: entry.name_by_user if entry.name_by_user else entry.name
+ for entry in self._device_entries
+ if self._can_replace_device(entry.id)
+ }
+
+ if devices:
+ data_schema.update(
+ {
+ vol.Optional(CONF_REPLACE_DEVICE): vol.In(devices),
+ }
+ )
+
+ return self.async_show_form(
+ step_id="set_device_options",
+ data_schema=vol.Schema(data_schema),
+ errors=errors,
+ )
+
+ async def _async_replace_device(self, replace_device):
+ """Migrate properties of a device into another."""
+ device_registry = self._device_registry
+ old_device = self._selected_device_entry_id
+ old_entry = device_registry.async_get(old_device)
+ device_registry.async_update_device(
+ replace_device,
+ area_id=old_entry.area_id,
+ name_by_user=old_entry.name_by_user,
+ )
+
+ old_device_data = self._get_device_data(old_device)
+ new_device_data = self._get_device_data(replace_device)
+
+ old_device_id = "_".join(x for x in old_device_data[CONF_DEVICE_ID])
+ new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID])
+
+ entity_registry = await async_get_entity_registry(self.hass)
+ entity_entries = async_entries_for_device(entity_registry, old_device)
+ entity_migration_map = {}
+ for entry in entity_entries:
+ unique_id = entry.unique_id
+ new_unique_id = unique_id.replace(old_device_id, new_device_id)
+
+ new_entity_id = entity_registry.async_get_entity_id(
+ entry.domain, entry.platform, new_unique_id
+ )
+
+ if new_entity_id is not None:
+ entity_migration_map[new_entity_id] = entry
+
+ for entry in entity_migration_map.values():
+ entity_registry.async_remove(entry.entity_id)
+
+ for entity_id, entry in entity_migration_map.items():
+ entity_registry.async_update_entity(
+ entity_id,
+ new_entity_id=entry.entity_id,
+ name=entry.name,
+ icon=entry.icon,
+ )
+
+ device_registry.async_remove_device(old_device)
+
+ def _can_add_device(self, new_rfx_obj):
+ """Check if device does not already exist."""
+ new_device_id = get_device_id(new_rfx_obj.device)
+ for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
+ rfx_obj = get_rfx_object(packet_id)
+ device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS))
+ if new_device_id == device_id:
+ return False
+
+ return True
+
+ def _can_replace_device(self, entry_id):
+ """Check if device can be replaced with selected device."""
+ device_data = self._get_device_data(entry_id)
+ event_code = device_data[CONF_EVENT_CODE]
+ rfx_obj = get_rfx_object(event_code)
+ if (
+ rfx_obj.device.packettype == self._selected_device_object.device.packettype
+ and rfx_obj.device.subtype == self._selected_device_object.device.subtype
+ and self._selected_device_event_code != event_code
+ ):
+ return True
+
+ return False
+
+ def _get_device_data(self, entry_id):
+ """Get event code based on device identifier."""
+ event_code = None
+ device_id = None
+ entry = self._device_registry.async_get(entry_id)
+ device_id = next(iter(entry.identifiers))[1:]
+ for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
+ if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id:
+ event_code = packet_id
+ break
+
+ data = {CONF_EVENT_CODE: event_code, CONF_DEVICE_ID: device_id}
+
+ return data
+
+ @callback
+ def update_config_data(self, global_options=None, devices=None):
+ """Update data in ConfigEntry."""
+ entry_data = self._config_entry.data.copy()
+ entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES])
+ if global_options:
+ entry_data.update(global_options)
+ if devices:
+ for event_code, options in devices.items():
+ if options is None:
+ entry_data[CONF_DEVICES].pop(event_code)
+ else:
+ entry_data[CONF_DEVICES][event_code] = options
+ self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._config_entry.entry_id)
+ )
+
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for RFXCOM RFXtrx."""
@@ -14,10 +406,190 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+ async def async_step_user(self, user_input=None):
+ """Step when user initializes a integration."""
+ await self.async_set_unique_id(DOMAIN)
+ self._abort_if_unique_id_configured()
+
+ errors = {}
+ if user_input is not None:
+ user_selection = user_input[CONF_TYPE]
+ if user_selection == "Serial":
+ return await self.async_step_setup_serial()
+
+ return await self.async_step_setup_network()
+
+ list_of_types = ["Serial", "Network"]
+
+ schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)})
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+ async def async_step_setup_network(self, user_input=None):
+ """Step when setting up network configuration."""
+ errors = {}
+
+ if user_input is not None:
+ host = user_input[CONF_HOST]
+ port = user_input[CONF_PORT]
+
+ try:
+ data = await self.async_validate_rfx(host=host, port=port)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+
+ if not errors:
+ return self.async_create_entry(title="RFXTRX", data=data)
+
+ schema = vol.Schema(
+ {vol.Required(CONF_HOST): str, vol.Required(CONF_PORT): int}
+ )
+ return self.async_show_form(
+ step_id="setup_network",
+ data_schema=schema,
+ errors=errors,
+ )
+
+ async def async_step_setup_serial(self, user_input=None):
+ """Step when setting up serial configuration."""
+ errors = {}
+
+ if user_input is not None:
+ user_selection = user_input[CONF_DEVICE]
+ if user_selection == CONF_MANUAL_PATH:
+ return await self.async_step_setup_serial_manual_path()
+
+ dev_path = await self.hass.async_add_executor_job(
+ get_serial_by_id, user_selection
+ )
+
+ try:
+ data = await self.async_validate_rfx(device=dev_path)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+
+ if not errors:
+ return self.async_create_entry(title="RFXTRX", data=data)
+
+ ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
+ list_of_ports = {}
+ for port in ports:
+ list_of_ports[
+ port.device
+ ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + (
+ f" - {port.manufacturer}" if port.manufacturer else ""
+ )
+ list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
+
+ schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(list_of_ports)})
+ return self.async_show_form(
+ step_id="setup_serial",
+ data_schema=schema,
+ errors=errors,
+ )
+
+ async def async_step_setup_serial_manual_path(self, user_input=None):
+ """Select path manually."""
+ errors = {}
+
+ if user_input is not None:
+ device = user_input[CONF_DEVICE]
+ try:
+ data = await self.async_validate_rfx(device=device)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+
+ if not errors:
+ return self.async_create_entry(title="RFXTRX", data=data)
+
+ schema = vol.Schema({vol.Required(CONF_DEVICE): str})
+ return self.async_show_form(
+ step_id="setup_serial_manual_path",
+ data_schema=schema,
+ errors=errors,
+ )
+
async def async_step_import(self, import_config=None):
"""Handle the initial step."""
entry = await self.async_set_unique_id(DOMAIN)
- if entry and import_config.items() != entry.data.items():
- self.hass.config_entries.async_update_entry(entry, data=import_config)
- return self.async_abort(reason="already_configured")
+ if entry:
+ if CONF_DEVICES not in entry.data:
+ # In version 0.113, devices key was not written to config entry. Update the entry with import data
+ self._abort_if_unique_id_configured(import_config)
+ else:
+ self._abort_if_unique_id_configured()
+
+ host = import_config[CONF_HOST]
+ port = import_config[CONF_PORT]
+ device = import_config[CONF_DEVICE]
+
+ try:
+ if host is not None:
+ await self.async_validate_rfx(host=host, port=port)
+ else:
+ await self.async_validate_rfx(device=device)
+ except CannotConnect:
+ return self.async_abort(reason="cannot_connect")
+
return self.async_create_entry(title="RFXTRX", data=import_config)
+
+ async def async_validate_rfx(self, host=None, port=None, device=None):
+ """Create data for rfxtrx entry."""
+ success = await self.hass.async_add_executor_job(
+ _test_transport, host, port, device
+ )
+ if not success:
+ raise CannotConnect
+
+ data = {
+ CONF_HOST: host,
+ CONF_PORT: port,
+ CONF_DEVICE: device,
+ CONF_AUTOMATIC_ADD: False,
+ CONF_DEVICES: {},
+ }
+ return data
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
+ """Get the options flow for this handler."""
+ return OptionsFlow(config_entry)
+
+
+def _test_transport(host, port, device):
+ """Construct a rfx object based on config."""
+ if port is not None:
+ try:
+ conn = rfxtrxmod.PyNetworkTransport((host, port))
+ except OSError:
+ return False
+
+ conn.close()
+ else:
+ try:
+ conn = rfxtrxmod.PySerialTransport(device)
+ except serial.serialutil.SerialException:
+ return False
+
+ if conn.serial is None:
+ return False
+
+ conn.close()
+
+ return True
+
+
+def get_serial_by_id(dev_path: str) -> str:
+ """Return a /dev/serial/by-id match for given device if available."""
+ by_id = "/dev/serial/by-id"
+ if not os.path.isdir(by_id):
+ return dev_path
+
+ for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
+ if os.path.realpath(path) == dev_path:
+ return path
+ return dev_path
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py
index c0436bfcf60489..404d344cc7191d 100644
--- a/homeassistant/components/rfxtrx/const.py
+++ b/homeassistant/components/rfxtrx/const.py
@@ -1,5 +1,14 @@
"""Constants for RFXtrx integration."""
+CONF_FIRE_EVENT = "fire_event"
+CONF_DATA_BITS = "data_bits"
+CONF_AUTOMATIC_ADD = "automatic_add"
+CONF_SIGNAL_REPETITIONS = "signal_repetitions"
+CONF_DEBUG = "debug"
+CONF_OFF_DELAY = "off_delay"
+
+CONF_REMOVE_DEVICE = "remove_device"
+CONF_REPLACE_DEVICE = "replace_device"
COMMAND_ON_LIST = [
"On",
diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py
index fc6ab6cbf15551..86950308f5550d 100644
--- a/homeassistant/components/rfxtrx/cover.py
+++ b/homeassistant/components/rfxtrx/cover.py
@@ -20,6 +20,11 @@
_LOGGER = logging.getLogger(__name__)
+def supported(event):
+ """Return whether an event supports cover."""
+ return event.device.known_to_be_rollershutter
+
+
async def async_setup_entry(
hass,
config_entry,
@@ -29,9 +34,6 @@ async def async_setup_entry(
discovery_info = config_entry.data
device_ids = set()
- def supported(event):
- return event.device.known_to_be_rollershutter
-
entities = []
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py
index 791cc158693ce9..33ee5ea4748ab2 100644
--- a/homeassistant/components/rfxtrx/light.py
+++ b/homeassistant/components/rfxtrx/light.py
@@ -28,6 +28,14 @@
SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS
+def supported(event):
+ """Return whether an event supports light."""
+ return (
+ isinstance(event.device, rfxtrxmod.LightingDevice)
+ and event.device.known_to_be_dimmable
+ )
+
+
async def async_setup_entry(
hass,
config_entry,
@@ -37,12 +45,6 @@ async def async_setup_entry(
discovery_info = config_entry.data
device_ids = set()
- def supported(event):
- return (
- isinstance(event.device, rfxtrxmod.LightingDevice)
- and event.device.known_to_be_dimmable
- )
-
# Add switch from config file
entities = []
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json
index 44b53ed0dacbb0..e62fc5c3c8311b 100644
--- a/homeassistant/components/rfxtrx/manifest.json
+++ b/homeassistant/components/rfxtrx/manifest.json
@@ -2,7 +2,7 @@
"domain": "rfxtrx",
"name": "RFXCOM RFXtrx",
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
- "requirements": ["pyRFXtrx==0.25"],
- "codeowners": ["@danielhiversen", "@elupus"],
- "config_flow": false
-}
\ No newline at end of file
+ "requirements": ["pyRFXtrx==0.26"],
+ "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
+ "config_flow": true
+}
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 4acde6b04506d1..81e0c60e05532e 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -9,7 +9,14 @@
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
)
-from homeassistant.const import CONF_DEVICES
+from homeassistant.const import (
+ CONF_DEVICES,
+ DEVICE_CLASS_CURRENT,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_VOLTAGE,
+)
from homeassistant.core import callback
from . import (
@@ -30,7 +37,7 @@ def _battery_convert(value):
"""Battery is given as a value between 0 and 9."""
if value is None:
return None
- return value * 10
+ return (value + 1) * 10
def _rssi_convert(value):
@@ -41,10 +48,17 @@ def _rssi_convert(value):
DEVICE_CLASSES = {
+ "Barometer": DEVICE_CLASS_PRESSURE,
"Battery numeric": DEVICE_CLASS_BATTERY,
- "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH,
+ "Current Ch. 1": DEVICE_CLASS_CURRENT,
+ "Current Ch. 2": DEVICE_CLASS_CURRENT,
+ "Current Ch. 3": DEVICE_CLASS_CURRENT,
+ "Energy usage": DEVICE_CLASS_POWER,
"Humidity": DEVICE_CLASS_HUMIDITY,
+ "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH,
"Temperature": DEVICE_CLASS_TEMPERATURE,
+ "Total usage": DEVICE_CLASS_ENERGY,
+ "Voltage": DEVICE_CLASS_VOLTAGE,
}
@@ -124,7 +138,7 @@ def __init__(self, device, device_id, data_type, event=None):
"""Initialize the sensor."""
super().__init__(device, device_id, event=event)
self.data_type = data_type
- self._unit_of_measurement = DATA_TYPES.get(data_type, "")
+ self._unit_of_measurement = DATA_TYPES.get(data_type)
self._name = f"{device.type_string} {device.id_string} {data_type}"
self._unique_id = "_".join(x for x in (*self._device_id, data_type))
diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json
index e19265dec325f4..9e97699915746d 100644
--- a/homeassistant/components/rfxtrx/strings.json
+++ b/homeassistant/components/rfxtrx/strings.json
@@ -1,9 +1,74 @@
{
- "config": {
- "step": {},
- "error": {},
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
- }
+ "title": "Rfxtrx",
+ "config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "type": "Connection type"
+ },
+ "title": "Select connection type"
+ },
+ "setup_network": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "port": "[%key:common::config_flow::data::port%]"
+ },
+ "title": "Select connection address"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Select device"
+ },
+ "title": "Device"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "[%key:common::config_flow::data::usb_path%]"
+ },
+ "title": "Path"
+ }
}
+ },
+ "options": {
+ "step": {
+ "prompt_options": {
+ "data": {
+ "debug": "Enable debugging",
+ "automatic_add": "Enable automatic add",
+ "event_code": "Enter event code to add",
+ "device": "Select device to configure",
+ "remove_device": "Select device to delete"
+ },
+ "title": "Rfxtrx Options"
+ },
+ "set_device_options": {
+ "data": {
+ "fire_event": "Enable device event",
+ "off_delay": "Off delay",
+ "off_delay_enabled": "Enable off delay",
+ "data_bit": "Number of data bits",
+ "command_on": "Data bits value for command on",
+ "command_off": "Data bits value for command off",
+ "signal_repetitions": "Number of signal repetitions",
+ "replace_device": "Select device to replace"
+ },
+ "title": "Configure device options"
+ }
+ },
+ "error": {
+ "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
+ "invalid_event_code": "Invalid event code",
+ "invalid_input_2262_on": "Invalid input for command on",
+ "invalid_input_2262_off": "Invalid input for command off",
+ "invalid_input_off_delay": "Invalid input for off delay",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ }
}
diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py
index bce5222b778c9f..5306921079499e 100644
--- a/homeassistant/components/rfxtrx/switch.py
+++ b/homeassistant/components/rfxtrx/switch.py
@@ -25,6 +25,16 @@
_LOGGER = logging.getLogger(__name__)
+def supported(event):
+ """Return whether an event supports switch."""
+ return (
+ isinstance(event.device, rfxtrxmod.LightingDevice)
+ and not event.device.known_to_be_dimmable
+ and not event.device.known_to_be_rollershutter
+ or isinstance(event.device, rfxtrxmod.RfyDevice)
+ )
+
+
async def async_setup_entry(
hass,
config_entry,
@@ -34,14 +44,6 @@ async def async_setup_entry(
discovery_info = config_entry.data
device_ids = set()
- def supported(event):
- return (
- isinstance(event.device, rfxtrxmod.LightingDevice)
- and not event.device.known_to_be_dimmable
- and not event.device.known_to_be_rollershutter
- or isinstance(event.device, rfxtrxmod.RfyDevice)
- )
-
# Add switch from config file
entities = []
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json
index 14e637f5f98553..5861878e0792b2 100644
--- a/homeassistant/components/rfxtrx/translations/ca.json
+++ b/homeassistant/components/rfxtrx/translations/ca.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat"
+ "already_configured": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "port": "Port"
+ },
+ "title": "Selecciona l'adre\u00e7a de connexi\u00f3"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Selecciona el dispositiu"
+ },
+ "title": "Dispositiu"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "Ruta del port USB del dispositiu"
+ },
+ "title": "Ruta"
+ },
+ "user": {
+ "data": {
+ "type": "Tipus de connexi\u00f3"
+ },
+ "title": "Selecciona el tipus de connexi\u00f3"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "El dispositiu ja est\u00e0 configurat",
+ "invalid_event_code": "Codi d'esdeveniment inv\u00e0lid",
+ "invalid_input_2262_off": "Entrada no v\u00e0lida per a l'ordre OFF",
+ "invalid_input_2262_on": "Entrada no v\u00e0lida per a l'ordre ON",
+ "invalid_input_off_delay": "Entrada no v\u00e0lida per al retard OFF",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Activa l'addici\u00f3 autom\u00e0tica",
+ "debug": "Activa la depuraci\u00f3",
+ "device": "Selecciona el dispositiu a configurar",
+ "event_code": "Introdueix el codi de l'esdeveniment a afegir",
+ "remove_device": "Selecciona el dispositiu a eliminar"
+ },
+ "title": "Opcions de Rfxtrx"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Valor dels bits de dades per a l'ordre OFF",
+ "command_on": "Valor dels bits de dades per a l'ordre ON",
+ "data_bit": "Nombre de bits de dades",
+ "fire_event": "Activa l'esdeveniment de dispositiu",
+ "off_delay": "Retard OFF",
+ "off_delay_enabled": "Activa el retard OFF",
+ "replace_device": "Selecciona el dispositiu a substituir",
+ "signal_repetitions": "Nombre de repeticions del senyal"
+ },
+ "title": "Configuraci\u00f3 de les opcions del dispositiu"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json
new file mode 100644
index 00000000000000..da1d200c2a2099
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/de.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/el.json b/homeassistant/components/rfxtrx/translations/el.json
new file mode 100644
index 00000000000000..fcf3a9ec83bca4
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/el.json
@@ -0,0 +1,67 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
+ },
+ "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
+ },
+ "setup_serial_manual_path": {
+ "title": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae"
+ },
+ "user": {
+ "data": {
+ "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_event_code": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2",
+ "invalid_input_2262_off": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae off",
+ "invalid_input_2262_on": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae on",
+ "invalid_input_off_delay": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2",
+ "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",
+ "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"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "\u03a4\u03b9\u03bc\u03ae bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae off",
+ "command_on": "\u03a4\u03b9\u03bc\u03ae bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae on",
+ "data_bit": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd",
+ "fire_event": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2",
+ "off_delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2",
+ "off_delay_enabled": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2",
+ "replace_device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7",
+ "signal_repetitions": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ae\u03c8\u03b5\u03c9\u03bd \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2"
+ },
+ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2"
+ }
+ }
+ },
+ "title": "Rfxtrx"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json
index 1344d2f69881f4..5bc019699b70a1 100644
--- a/homeassistant/components/rfxtrx/translations/en.json
+++ b/homeassistant/components/rfxtrx/translations/en.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Already configured. Only a single configuration possible.",
+ "cannot_connect": "Failed to connect"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Select connection address"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Select device"
+ },
+ "title": "Device"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB Device Path"
+ },
+ "title": "Path"
+ },
+ "user": {
+ "data": {
+ "type": "Connection type"
+ },
+ "title": "Select connection type"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "Device is already configured",
+ "invalid_event_code": "Invalid event code",
+ "invalid_input_2262_off": "Invalid input for command off",
+ "invalid_input_2262_on": "Invalid input for command on",
+ "invalid_input_off_delay": "Invalid input for off delay",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Enable automatic add",
+ "debug": "Enable debugging",
+ "device": "Select device to configure",
+ "event_code": "Enter event code to add",
+ "remove_device": "Select device to delete"
+ },
+ "title": "Rfxtrx Options"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Data bits value for command off",
+ "command_on": "Data bits value for command on",
+ "data_bit": "Number of data bits",
+ "fire_event": "Enable device event",
+ "off_delay": "Off delay",
+ "off_delay_enabled": "Enable off delay",
+ "replace_device": "Select device to replace",
+ "signal_repetitions": "Number of signal repetitions"
+ },
+ "title": "Configure device options"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json
index e8e23bf8343ee3..ee4cc841d1492b 100644
--- a/homeassistant/components/rfxtrx/translations/es.json
+++ b/homeassistant/components/rfxtrx/translations/es.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "El dispositivo ya est\u00e1 configurado"
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Puerto"
+ },
+ "title": "Seleccionar la direcci\u00f3n de conexi\u00f3n"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Seleccionar dispositivo"
+ },
+ "title": "Dispositivo"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "Ruta del dispositivo USB"
+ },
+ "title": "Ruta"
+ },
+ "user": {
+ "data": {
+ "type": "Tipo de conexi\u00f3n"
+ },
+ "title": "Seleccionar tipo de conexi\u00f3n"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "El dispositivo ya est\u00e1 configurado",
+ "invalid_event_code": "C\u00f3digo de evento no v\u00e1lido",
+ "invalid_input_2262_off": "Entrada inv\u00e1lida para el comando de apagado",
+ "invalid_input_2262_on": "Entrada inv\u00e1lida para el comando de encendido",
+ "invalid_input_off_delay": "Entrada inv\u00e1lida para el retardo de apagado",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Activar la adici\u00f3n autom\u00e1tica",
+ "debug": "Activar la depuraci\u00f3n",
+ "device": "Seleccionar dispositivo para configurar",
+ "event_code": "Introducir el c\u00f3digo de evento para a\u00f1adir",
+ "remove_device": "Seleccione el dispositivo a eliminar"
+ },
+ "title": "Opciones de Rfxtrx"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Valor de bits de datos para comando apagado",
+ "command_on": "Valor de bits de datos para comando activado",
+ "data_bit": "N\u00famero de bits de datos",
+ "fire_event": "Habilitar evento del dispositivo",
+ "off_delay": "Retraso de apagado",
+ "off_delay_enabled": "Activar retardo de apagado",
+ "replace_device": "Seleccione el dispositivo que desea reemplazar",
+ "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al"
+ },
+ "title": "Configurar las opciones del dispositivo"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json
new file mode 100644
index 00000000000000..a7b66994d4b102
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/et.json
@@ -0,0 +1,69 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u00dchendus nurjus"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus"
+ },
+ "step": {
+ "setup_network": {
+ "title": "Vali \u00fchenduse aadress"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Vali seade"
+ },
+ "title": "Seade"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB seadme rada"
+ },
+ "title": "Rada"
+ },
+ "user": {
+ "data": {
+ "type": "\u00dchenduse t\u00fc\u00fcp"
+ },
+ "title": "Vali \u00fchenduse t\u00fc\u00fcp"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud",
+ "invalid_event_code": "Vale s\u00fcndmuse kood",
+ "invalid_input_2262_off": "Vigane sisestus v\u00e4ljal\u00fclitamiseks",
+ "invalid_input_2262_on": "Vigane sisestus sissel\u00fclitamisks",
+ "invalid_input_off_delay": "Vigane sisestus v\u00e4ljal\u00fclitamise viivituseks",
+ "unknown": "Tundmatu viga"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Luba automaatne lisamine",
+ "debug": "Luba silumine",
+ "device": "Vali seadistatav seade",
+ "event_code": "Sisesta lisatava s\u00fcndmuse kood",
+ "remove_device": "Vali eemaldatav seade"
+ },
+ "title": "Rfxtrx valikud"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "V\u00e4ljal\u00fclitamise k\u00e4su andmebittide v\u00e4\u00e4rtus",
+ "command_on": "Sissel\u00fclitamise k\u00e4su andmebittide v\u00e4\u00e4rtus",
+ "data_bit": "Andmebittide arv",
+ "fire_event": "Luba seadme s\u00fcndmus",
+ "off_delay": "V\u00e4ljal\u00fclitamise viivitus",
+ "off_delay_enabled": "Luba v\u00e4ljal\u00fclitusviivitus",
+ "replace_device": "Vali asendav seade",
+ "signal_repetitions": "Signaali korduste arv"
+ },
+ "title": "Seadista seadme valikud"
+ }
+ }
+ },
+ "title": "Rfxtrx"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json
index c4bc0d48b1a08b..5155bf6dacf6cb 100644
--- a/homeassistant/components/rfxtrx/translations/fr.json
+++ b/homeassistant/components/rfxtrx/translations/fr.json
@@ -2,6 +2,30 @@
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "step": {
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "Chemin du p\u00e9riph\u00e9rique USB"
+ }
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "invalid_event_code": "Code d'\u00e9v\u00e9nement non valide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "set_device_options": {
+ "data": {
+ "replace_device": "S\u00e9lectionnez l'appareil \u00e0 remplacer",
+ "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal"
+ },
+ "title": "Configurer les options de l'appareil"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json
index cbea61b08cbedd..678047af145385 100644
--- a/homeassistant/components/rfxtrx/translations/it.json
+++ b/homeassistant/components/rfxtrx/translations/it.json
@@ -1,17 +1,80 @@
{
"config": {
"abort": {
- "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ "already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
+ "cannot_connect": "Impossibile connettersi"
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"one": "uno",
"other": "altri"
},
"step": {
"one": "uno",
- "other": "altri"
+ "other": "altri",
+ "setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Porta"
+ },
+ "title": "Selezionare l'indirizzo di connessione"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Selezionare il dispositivo"
+ },
+ "title": "Dispositivo"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "Percorso del dispositivo USB"
+ },
+ "title": "Percorso"
+ },
+ "user": {
+ "data": {
+ "type": "Tipo di connessione"
+ },
+ "title": "Selezionare il tipo di connessione"
+ }
}
},
"one": "uno",
- "other": "altri"
+ "options": {
+ "error": {
+ "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "invalid_event_code": "Codice evento non valido",
+ "invalid_input_2262_off": "Immissione non valida per il comando disattivato",
+ "invalid_input_2262_on": "Immissione non valida per il comando attivato",
+ "invalid_input_off_delay": "Immissione non valida per il ritardo di spegnimento",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Attivare l'aggiunta automatica",
+ "debug": "Attivare il debug",
+ "device": "Selezionare il dispositivo da configurare",
+ "event_code": "Inserire il codice dell'evento da aggiungere",
+ "remove_device": "Selezionare il dispositivo da cancellare"
+ },
+ "title": "Opzioni Rfxtrx"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Valore dei bit di dati per il comando disattivato",
+ "command_on": "Valore dei bit di dati per il comando attivato",
+ "data_bit": "Numero di bit di dati",
+ "fire_event": "Abilita evento dispositivo",
+ "off_delay": "Ritardo di spegnimento",
+ "off_delay_enabled": "Attivare il ritardo di spegnimento",
+ "replace_device": "Selezionare il dispositivo da sostituire",
+ "signal_repetitions": "Numero di ripetizioni del segnale"
+ },
+ "title": "Configurare le opzioni del dispositivo"
+ }
+ }
+ },
+ "other": "altri",
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json
new file mode 100644
index 00000000000000..aa8512da2850af
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/ko.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/lb.json b/homeassistant/components/rfxtrx/translations/lb.json
index 6469543442e1ff..6e9b0fcb9c007c 100644
--- a/homeassistant/components/rfxtrx/translations/lb.json
+++ b/homeassistant/components/rfxtrx/translations/lb.json
@@ -1,7 +1,69 @@
{
"config": {
"abort": {
- "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Verbindung's Adress auswielen"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Apparat auswielen"
+ },
+ "title": "Apparat"
+ },
+ "setup_serial_manual_path": {
+ "title": "Pad"
+ },
+ "user": {
+ "data": {
+ "type": "Typ vun der Verbindung"
+ },
+ "title": "Typ vun der Verbindung auswielen"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "invalid_event_code": "Ong\u00ebltegen Evenement Code",
+ "invalid_input_2262_off": "Ong\u00eblteg Agab fir Kommando Aus",
+ "invalid_input_2262_on": "Ong\u00eblteg Agab fir Kommando Un",
+ "invalid_input_off_delay": "Ong\u00eblteg Agab fir Aus Verz\u00f6gerung"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Aktiv\u00e9ier automatesch dob\u00e4isetzen",
+ "debug": "Aktiv\u00e9ier Debuggen",
+ "device": "Wiel een Apparat fir ze konfigur\u00e9ieren",
+ "event_code": "Evenement Code aginn fir dob\u00e4izesetzen",
+ "remove_device": "Wiel een Apparat fir ze l\u00e4schen"
+ },
+ "title": "Rfxtrx Optioune"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Data bits W\u00e4ert fir Kommando aus",
+ "command_on": "Data bits W\u00e4ert fir Kommando un",
+ "data_bit": "Unzuel vun Data Bits",
+ "fire_event": "Aktiv\u00e9ier Apparat Evenement",
+ "off_delay": "Aus Verz\u00f6gerung",
+ "off_delay_enabled": "Aktiv\u00e9iert Aus Verz\u00f6gerung",
+ "replace_device": "Wiel een Apparat fir ze ersetzen",
+ "signal_repetitions": "Zuel vu Signalwidderhuelungen"
+ },
+ "title": "Apparat Optioune konfigur\u00e9ieren"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json
index 6ba5a1f39783f4..c89ddc0bbbf5eb 100644
--- a/homeassistant/components/rfxtrx/translations/no.json
+++ b/homeassistant/components/rfxtrx/translations/no.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert"
+ "already_configured": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
+ "cannot_connect": "Tilkobling mislyktes."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes."
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Vert",
+ "port": "Port"
+ },
+ "title": "Velg tilkoblingsadresse"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Velg enhet"
+ },
+ "title": "Enhet"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB enhetsbane"
+ },
+ "title": "Sti"
+ },
+ "user": {
+ "data": {
+ "type": "Tilkoblingstype"
+ },
+ "title": "Velg tilkoblingstype"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "Enheten er allerede konfigurert",
+ "invalid_event_code": "Ugyldig hendelseskode",
+ "invalid_input_2262_off": "Ugyldig inndata for kommando av",
+ "invalid_input_2262_on": "Ugyldig inndata for kommando p\u00e5",
+ "invalid_input_off_delay": "Ugyldig inndata for av forsinkelse",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Aktivere automatisk legg til",
+ "debug": "Aktiver feils\u00f8king",
+ "device": "Velg enhet du vil konfigurere",
+ "event_code": "Angi hendelseskode for \u00e5 legge til",
+ "remove_device": "Velg enhet du vil slette"
+ },
+ "title": "Rfxtrx Alternativer"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Databiter-verdi for kommando av",
+ "command_on": "Databiter-verdi for kommando p\u00e5",
+ "data_bit": "Antall databiter",
+ "fire_event": "Aktiver enhetshendelse",
+ "off_delay": "Av forsinkelse",
+ "off_delay_enabled": "Aktiver av forsinkelse",
+ "replace_device": "Velg enheten du vil erstatte",
+ "signal_repetitions": "Antall signalrepetisjoner"
+ },
+ "title": "Konfigurer enhetsalternativer"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json
new file mode 100644
index 00000000000000..77fa686542f5ac
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/pl.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json
index 4ad85f691befa0..0926734657d23c 100644
--- a/homeassistant/components/rfxtrx/translations/ru.json
+++ b/homeassistant/components/rfxtrx/translations/ru.json
@@ -1,7 +1,74 @@
{
"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_configured": "\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.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "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": {
+ "setup_network": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
+ },
+ "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ },
+ "title": "\u041f\u0443\u0442\u044c"
+ },
+ "user": {
+ "data": {
+ "type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "\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.",
+ "invalid_event_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u043e\u0431\u044b\u0442\u0438\u044f.",
+ "invalid_input_2262_off": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0432\u0432\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
+ "invalid_input_2262_on": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0432\u0432\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
+ "invalid_input_off_delay": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0432\u0432\u043e\u0434 \u0434\u043b\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435",
+ "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",
+ "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"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0431\u0438\u0442\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0431\u0438\u0442\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "data_bit": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0431\u0438\u0442 \u0434\u0430\u043d\u043d\u044b\u0445",
+ "fire_event": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
+ "off_delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "replace_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b",
+ "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ 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 1ab3e1f720fb13..dac2a6850b0933 100644
--- a/homeassistant/components/rfxtrx/translations/zh-Hant.json
+++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "title": "\u9078\u64c7\u9023\u7dda\u4f4d\u5740"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\u9078\u64c7\u8a2d\u5099"
+ },
+ "title": "\u8a2d\u5099"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB \u8a2d\u5099\u8def\u5f91"
+ },
+ "title": "\u8def\u5f91"
+ },
+ "user": {
+ "data": {
+ "type": "\u9023\u7dda\u985e\u578b"
+ },
+ "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b"
+ }
}
- }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548",
+ "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548",
+ "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548",
+ "invalid_input_off_delay": "\u5ef6\u8aa4\u8f38\u5165\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "\u958b\u555f\u81ea\u52d5\u65b0\u589e",
+ "debug": "\u958b\u555f\u9664\u932f",
+ "device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a",
+ "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e",
+ "remove_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u522a\u9664"
+ },
+ "title": "Rfxtrx \u9078\u9805"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "\u547d\u4ee4\u95dc\u9589\u7684\u8cc7\u6599\u4f4d\u5143\u503c",
+ "command_on": "\u547d\u4ee4\u958b\u555f\u7684\u8cc7\u6599\u4f4d\u5143\u503c",
+ "data_bit": "\u8cc7\u6599\u4f4d\u5143\u6578",
+ "fire_event": "\u958b\u555f\u8a2d\u5099\u4e8b\u4ef6",
+ "off_delay": "\u5ef6\u9072",
+ "off_delay_enabled": "\u958b\u555f\u5ef6\u9072",
+ "replace_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u53d6\u4ee3",
+ "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578"
+ },
+ "title": "\u8a2d\u5b9a\u8a2d\u5099\u9078\u9805"
+ }
+ }
+ },
+ "title": "Rfxtrx"
}
\ No newline at end of file
diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py
index fa303b9437888a..ccec9e6ad360b7 100644
--- a/homeassistant/components/ring/binary_sensor.py
+++ b/homeassistant/components/ring/binary_sensor.py
@@ -2,7 +2,11 @@
from datetime import datetime
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OCCUPANCY,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from . import DOMAIN
@@ -12,8 +16,12 @@
# Sensor types: Name, category, device_class
SENSOR_TYPES = {
- "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"],
- "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"],
+ "ding": ["Ding", ["doorbots", "authorized_doorbots"], DEVICE_CLASS_OCCUPANCY],
+ "motion": [
+ "Motion",
+ ["doorbots", "authorized_doorbots", "stickup_cams"],
+ DEVICE_CLASS_MOTION,
+ ],
}
diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py
index 24a5cd3b6fb75b..f59c3b2e61d2a7 100644
--- a/homeassistant/components/ring/sensor.py
+++ b/homeassistant/components/ring/sensor.py
@@ -1,7 +1,7 @@
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
import logging
-from homeassistant.const import PERCENTAGE
+from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
@@ -256,7 +256,7 @@ def device_state_attributes(self):
"wifi_signal_strength": [
"WiFi Signal Strength",
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
- "dBm",
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
"wifi",
None,
"signal_strength",
diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json
index 04b19b6661478b..c5b448ad68b2d5 100644
--- a/homeassistant/components/ring/strings.json
+++ b/homeassistant/components/ring/strings.json
@@ -16,11 +16,11 @@
}
},
"error": {
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/ring/translations/et.json b/homeassistant/components/ring/translations/et.json
new file mode 100644
index 00000000000000..255510c130ca95
--- /dev/null
+++ b/homeassistant/components/ring/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/translations/pl.json b/homeassistant/components/ring/translations/pl.json
index 96aa7d391591e8..b095647d06cefa 100644
--- a/homeassistant/components/ring/translations/pl.json
+++ b/homeassistant/components/ring/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"2fa": {
diff --git a/homeassistant/components/ring/translations/ru.json b/homeassistant/components/ring/translations/ru.json
index aa3b383bbce1c4..fb8c22c39af644 100644
--- a/homeassistant/components/ring/translations/ru.json
+++ b/homeassistant/components/ring/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json
index 80b132b0fb2618..7f13af252f3d42 100644
--- a/homeassistant/components/risco/manifest.json
+++ b/homeassistant/components/risco/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/risco",
"requirements": [
- "pyrisco==0.3.0"
+ "pyrisco==0.3.1"
],
"codeowners": [
"@OnFreund"
diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json
index dc6031e0ad31e2..d98273bf1854c5 100644
--- a/homeassistant/components/risco/strings.json
+++ b/homeassistant/components/risco/strings.json
@@ -5,7 +5,7 @@
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
- "pin": "Pin code"
+ "pin": "[%key:common::config_flow::data::pin%]"
}
}
},
diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json
new file mode 100644
index 00000000000000..b0bae40bf4e6ec
--- /dev/null
+++ b/homeassistant/components/risco/translations/de.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "risco_to_ha": {
+ "data": {
+ "A": "Gruppe A",
+ "B": "Gruppe B",
+ "C": "Gruppe C",
+ "D": "Gruppe D"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json
new file mode 100644
index 00000000000000..c38cfc72cc19d0
--- /dev/null
+++ b/homeassistant/components/risco/translations/el.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7",
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u0395\u03ba\u03c4\u03cc\u03c2",
+ "armed_home": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u0395\u03bd\u03c4\u03cc\u03c2",
+ "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bd\u03cd\u03c7\u03c4\u03b1\u03c2"
+ },
+ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b8\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03cc Risco \u03ba\u03b1\u03c4\u03ac \u03c4\u03bf\u03bd \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd Home Assistant",
+ "title": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd Home Assistant \u03c3\u03b5 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Risco"
+ },
+ "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",
+ "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"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "\u039f\u03bc\u03ac\u03b4\u03b1 \u0391",
+ "B": "\u039f\u03bc\u03ac\u03b4\u03b1 \u0392",
+ "C": "\u039f\u03bc\u03ac\u03b4\u03b1 \u0393",
+ "D": "\u039f\u03bc\u03ac\u03b4\u03b1 \u0394",
+ "arm": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 (\u0395\u039a\u03a4\u039f\u03a3)",
+ "partial_arm": "\u039c\u03b5\u03c1\u03b9\u03ba\u03ce\u03c2 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 (\u0395\u039d\u03a4\u039f\u03a3)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/risco/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json
new file mode 100644
index 00000000000000..37d9a61307b062
--- /dev/null
+++ b/homeassistant/components/risco/translations/ko.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)",
+ "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)",
+ "armed_home": "\uc9d1\uc548 \uacbd\ube44\uc911",
+ "armed_night": "\uc57c\uac04 \uacbd\ube44\uc911"
+ },
+ "description": "Home Assistant \uc54c\ub78c\uc744 \ud65c\uc131\ud654 \ud560 \ub54c Risco \uc54c\ub78c\uc758 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.",
+ "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\ub85c \ub9e4\ud551"
+ },
+ "init": {
+ "data": {
+ "scan_interval": "Risco\ub97c \ud3f4\ub9c1\ud558\ub294 \ube48\ub3c4 (\ucd08)"
+ }
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "\uadf8\ub8f9 A",
+ "B": "\uadf8\ub8f9 B",
+ "C": "\uadf8\ub8f9 C",
+ "D": "\uadf8\ub8f9 D",
+ "arm": "\uacbd\ube44\uc911(\uc678\ucd9c)",
+ "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc124\uc815 (\uc7ac\uc2e4)"
+ },
+ "description": "Risco\uc5d0\uc11c\ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0 \ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4.",
+ "title": "Risco \uc0c1\ud0dc\ub97c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \uc0c1\ud0dc\uc5d0 \ub9e4\ud551"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json
index 933af1f4a38c55..985112bf4461f4 100644
--- a/homeassistant/components/risco/translations/lb.json
+++ b/homeassistant/components/risco/translations/lb.json
@@ -20,12 +20,27 @@
},
"options": {
"step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "Aktiv\u00e9iert \u00cbnnerwee",
+ "armed_home": "Aktiv\u00e9iert Doheem",
+ "armed_night": "Aktiv\u00e9iert Nuecht"
+ }
+ },
"init": {
"data": {
"code_arm_required": "Pin Code n\u00e9ideg fir unzeschalten",
"code_disarm_required": "Pin Code n\u00e9ideg fir auszeschalten"
},
"title": "Optioune konfigur\u00e9ieren"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "Grupp A",
+ "B": "Grupp B",
+ "C": "Grupp C",
+ "D": "Grupp D"
+ }
}
}
}
diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json
new file mode 100644
index 00000000000000..614a896c3f8abe
--- /dev/null
+++ b/homeassistant/components/risco/translations/nl.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "pin": "Pincode",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "Ingeschakeld weg",
+ "armed_custom_bypass": "Ingeschakeld met overbrugging(en)",
+ "armed_home": "Ingeschakeld thuis",
+ "armed_night": "Ingeschakeld nacht"
+ }
+ },
+ "init": {
+ "data": {
+ "code_arm_required": "Pincode vereist om in te schakelen",
+ "code_disarm_required": "Pincode vereist om uit te schakelen"
+ },
+ "title": "Configureer opties"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "Groep A",
+ "B": "Groep B",
+ "C": "Groep C",
+ "D": "Groep D"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json
index 9859923d31e6a1..92deb2da70a30a 100644
--- a/homeassistant/components/risco/translations/pl.json
+++ b/homeassistant/components/risco/translations/pl.json
@@ -1,20 +1,55 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
"data": {
"password": "Has\u0142o",
+ "pin": "Kod PIN",
"username": "Nazwa u\u017cytkownika"
}
}
}
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "Uzbrojony (pod nieobecno\u015b\u0107)",
+ "armed_custom_bypass": "Uzbrojony (cz\u0119\u015bciowo)",
+ "armed_home": "Uzbrojony (obecny)",
+ "armed_night": "Uzbrojony (noc)"
+ },
+ "description": "Wybierz stan, w kt\u00f3rym chcesz ustawi\u0107 alarm Risco podczas uzbrajania alarmu w Home Assistant",
+ "title": "Mapuj stany Home Assistant do stan\u00f3w alarmu Risco"
+ },
+ "init": {
+ "data": {
+ "code_arm_required": "Wymagaj kodu PIN do uzbrojenia",
+ "code_disarm_required": "Wymagaj kodu PIN do rozbrojenia",
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 od\u015bwie\u017cania (w sekundach)"
+ },
+ "title": "Opcje"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "Grupa A",
+ "B": "Grupa B",
+ "C": "Grupa C",
+ "D": "Grupa D",
+ "arm": "Uzbrojony (pod nieobecno\u015b\u0107)",
+ "partial_arm": "Uzbrojony (obecny)"
+ },
+ "description": "Wybierz stan, kt\u00f3ry b\u0119dzie raportowa\u0142 alarm Home Assistant, dla ka\u017cdego stanu zg\u0142oszonego przez Risco",
+ "title": "Mapuj stany Risco do stan\u00f3w Home Assistant"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py
index f6f8c8976f1333..b5be3e99d9a094 100644
--- a/homeassistant/components/roku/browse_media.py
+++ b/homeassistant/components/roku/browse_media.py
@@ -13,8 +13,13 @@
CONTENT_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
- MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
+ MEDIA_TYPE_APPS: MEDIA_CLASS_APP,
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
+ MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL,
+}
+
+CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
+ MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY,
}
@@ -37,6 +42,7 @@ def build_item_response(coordinator, payload):
thumbnail = None
title = None
media = None
+ children_media_class = None
if search_type == MEDIA_TYPE_APPS:
title = "Apps"
@@ -44,6 +50,7 @@ def build_item_response(coordinator, payload):
{"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP}
for item in coordinator.data.apps
]
+ children_media_class = MEDIA_CLASS_APP
elif search_type == MEDIA_TYPE_CHANNELS:
title = "Channels"
media = [
@@ -54,18 +61,22 @@ def build_item_response(coordinator, payload):
}
for item in coordinator.data.channels
]
+ children_media_class = MEDIA_CLASS_CHANNEL
if media is None:
return None
return BrowseMedia(
- media_class=MEDIA_CLASS_DIRECTORY,
+ media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
+ search_type, MEDIA_CLASS_DIRECTORY
+ ),
media_content_id=search_id,
media_content_type=search_type,
title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True,
children=[item_payload(item, coordinator) for item in media],
+ children_media_class=children_media_class,
thumbnail=thumbnail,
)
@@ -148,7 +159,5 @@ def library_payload(coordinator):
for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_CHANNEL
- else:
- library_info.children_media_class = MEDIA_CLASS_DIRECTORY
return library_info
diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json
index 7ca0148ce35c1f..21c969876e7720 100644
--- a/homeassistant/components/roku/translations/pl.json
+++ b/homeassistant/components/roku/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"flow_title": "Roku: {name}",
"step": {
diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json
index d22a6e0509cf90..cbe7c06ae36fec 100644
--- a/homeassistant/components/roomba/strings.json
+++ b/homeassistant/components/roomba/strings.json
@@ -14,7 +14,7 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
@@ -27,4 +27,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json
index 3d7efbe53ae43b..af358678144949 100644
--- a/homeassistant/components/roomba/translations/ca.json
+++ b/homeassistant/components/roomba/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar"
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"user": {
diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json
index 677ea5f4749bbb..b6222f2adf8a34 100644
--- a/homeassistant/components/roomba/translations/en.json
+++ b/homeassistant/components/roomba/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Failed to connect, please try again"
+ "cannot_connect": "Failed to connect"
},
"step": {
"user": {
diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json
index a8ff72b2ed52c6..1ec97dd3842e08 100644
--- a/homeassistant/components/roomba/translations/fr.json
+++ b/homeassistant/components/roomba/translations/fr.json
@@ -12,6 +12,7 @@
"host": "Nom d'h\u00f4te ou adresse IP",
"password": "Mot de passe"
},
+ "description": "La r\u00e9cup\u00e9ration du BLID et du mot de passe est actuellement un processus manuel. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 l'adresse: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"title": "Se connecter \u00e0 l'appareil"
}
}
diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json
index babd2082b3cbdb..d109aa8bcc099c 100644
--- a/homeassistant/components/roomba/translations/it.json
+++ b/homeassistant/components/roomba/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare"
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"user": {
diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json
index b780f3e718b2c1..6dccb358dd27a8 100644
--- a/homeassistant/components/roomba/translations/no.json
+++ b/homeassistant/components/roomba/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen"
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"user": {
diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json
index 12217a1bc3f0b7..ee1192f69ec63c 100644
--- a/homeassistant/components/roomba/translations/ru.json
+++ b/homeassistant/components/roomba/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437."
+ "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": {
diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json
index 5e51af5efb5237..794c67454fbd27 100644
--- a/homeassistant/components/roomba/translations/zh-Hant.json
+++ b/homeassistant/components/roomba/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
"user": {
diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json
new file mode 100644
index 00000000000000..b73dc4d64444d1
--- /dev/null
+++ b/homeassistant/components/roon/translations/de.json
@@ -0,0 +1,3 @@
+{
+ "title": "Roon"
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json
new file mode 100644
index 00000000000000..4a17e5f128893a
--- /dev/null
+++ b/homeassistant/components/roon/translations/el.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "duplicate_entry": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af."
+ },
+ "step": {
+ "link": {
+ "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.",
+ "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon"
+ },
+ "user": {
+ "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.",
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon"
+ }
+ }
+ },
+ "title": "Roon"
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/roon/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json
index 50c22e9e2560f3..cdffd6e88aefbc 100644
--- a/homeassistant/components/roon/translations/ko.json
+++ b/homeassistant/components/roon/translations/ko.json
@@ -4,8 +4,22 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "duplicate_entry": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\ub294 \uc774\ubbf8 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "link": {
+ "description": "Roon\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8\ub97c \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4. \uc81c\ucd9c\uc744 \ud074\ub9ad \ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c HomeAssistant\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4.",
+ "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ },
+ "description": "Roon \uc11c\ubc84 Hostname \ub610\ub294 IP\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624.",
+ "title": "Roon \uc11c\ubc84 \uad6c\uc131"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json
new file mode 100644
index 00000000000000..15fc480be6787b
--- /dev/null
+++ b/homeassistant/components/roon/translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "duplicate_entry": "Ten adres hosta zosta\u0142 ju\u017c dodany.",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "link": {
+ "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Prze\u015blij\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).",
+ "title": "Autoryzuj Home Assistant w Roon"
+ },
+ "user": {
+ "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP swojego serwera Roon.",
+ "title": "Konfiguracja serwera Roon"
+ }
+ }
+ },
+ "title": "Roon"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py
new file mode 100644
index 00000000000000..993d0b313c0a22
--- /dev/null
+++ b/homeassistant/components/rpi_power/__init__.py
@@ -0,0 +1,21 @@
+"""The Raspberry Pi Power Supply Checker integration."""
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Raspberry Pi Power Supply Checker component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Raspberry Pi Power Supply Checker from a config entry."""
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "binary_sensor")
+ )
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ return await hass.config_entries.async_forward_entry_unload(entry, "binary_sensor")
diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py
new file mode 100644
index 00000000000000..79ef36e891a868
--- /dev/null
+++ b/homeassistant/components/rpi_power/binary_sensor.py
@@ -0,0 +1,73 @@
+"""
+A sensor platform which detects underruns and capped status from the official Raspberry Pi Kernel.
+
+Minimal Kernel needed is 4.14+
+"""
+import logging
+
+from rpi_bad_power import new_under_voltage
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+_LOGGER = logging.getLogger(__name__)
+
+DESCRIPTION_NORMALIZED = "Voltage normalized. Everything is working as intended."
+DESCRIPTION_UNDER_VOLTAGE = "Under-voltage was detected. Consider getting a uninterruptible power supply for your Raspberry Pi."
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+):
+ """Set up rpi_power binary sensor."""
+ under_voltage = await hass.async_add_executor_job(new_under_voltage)
+ async_add_entities([RaspberryChargerBinarySensor(under_voltage)], True)
+
+
+class RaspberryChargerBinarySensor(BinarySensorEntity):
+ """Binary sensor representing the rpi power status."""
+
+ def __init__(self, under_voltage):
+ """Initialize the binary sensor."""
+ self._under_voltage = under_voltage
+ self._is_on = None
+ self._last_is_on = False
+
+ def update(self):
+ """Update the state."""
+ self._is_on = self._under_voltage.get()
+ if self._is_on != self._last_is_on:
+ if self._is_on:
+ _LOGGER.warning(DESCRIPTION_UNDER_VOLTAGE)
+ else:
+ _LOGGER.info(DESCRIPTION_NORMALIZED)
+ self._last_is_on = self._is_on
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return "rpi_power" # only one sensor possible
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return "RPi Power status"
+
+ @property
+ def is_on(self):
+ """Return if there is a problem detected."""
+ return self._is_on
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return "mdi:raspberry-pi"
+
+ @property
+ def device_class(self):
+ """Return the class of this device."""
+ return DEVICE_CLASS_PROBLEM
diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py
new file mode 100644
index 00000000000000..9924ebf0440cf2
--- /dev/null
+++ b/homeassistant/components/rpi_power/config_flow.py
@@ -0,0 +1,41 @@
+"""Config flow for Raspberry Pi Power Supply Checker."""
+from typing import Any, Dict, Optional
+
+from rpi_bad_power import new_under_voltage
+
+from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler
+
+from .const import DOMAIN
+
+
+async def _async_supported(hass: HomeAssistant) -> bool:
+ """Return if the system supports under voltage detection."""
+ under_voltage = await hass.async_add_executor_job(new_under_voltage)
+ return under_voltage is not None
+
+
+class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN):
+ """Discovery flow handler."""
+
+ VERSION = 1
+
+ def __init__(self) -> None:
+ """Set up config flow."""
+ super().__init__(
+ DOMAIN,
+ "Raspberry Pi Power Supply Checker",
+ _async_supported,
+ config_entries.CONN_CLASS_LOCAL_POLL,
+ )
+
+ async def async_step_onboarding(
+ self, data: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """Handle a flow initialized by onboarding."""
+ has_devices = await self._discovery_function(self.hass)
+
+ if not has_devices:
+ return self.async_abort(reason="no_devices_found")
+ return self.async_create_entry(title=self._title, data={})
diff --git a/homeassistant/components/rpi_power/const.py b/homeassistant/components/rpi_power/const.py
new file mode 100644
index 00000000000000..98cfc438903759
--- /dev/null
+++ b/homeassistant/components/rpi_power/const.py
@@ -0,0 +1,3 @@
+"""Constants for Raspberry Pi Power Supply Checker."""
+
+DOMAIN = "rpi_power"
diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json
new file mode 100644
index 00000000000000..e0d2a6424e8f09
--- /dev/null
+++ b/homeassistant/components/rpi_power/manifest.json
@@ -0,0 +1,13 @@
+{
+ "domain": "rpi_power",
+ "name": "Raspberry Pi Power Supply Checker",
+ "documentation": "https://www.home-assistant.io/integrations/rpi_power",
+ "codeowners": [
+ "@shenxn",
+ "@swetoast"
+ ],
+ "requirements": [
+ "rpi-bad-power==0.0.3"
+ ],
+ "config_flow": true
+}
diff --git a/homeassistant/components/rpi_power/strings.json b/homeassistant/components/rpi_power/strings.json
new file mode 100644
index 00000000000000..a9cd6c2d907a61
--- /dev/null
+++ b/homeassistant/components/rpi_power/strings.json
@@ -0,0 +1,14 @@
+{
+ "title": "Raspberry Pi Power Supply Checker",
+ "config": {
+ "step": {
+ "confirm": {
+ "description": "[%key:common::config_flow::description::confirm_setup%]"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/ca.json b/homeassistant/components/rpi_power/translations/ca.json
new file mode 100644
index 00000000000000..c53fa570b7eccf
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/ca.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No s'ha trobat la classe de sistema necess\u00e0ria per a aquest component, assegura't que el nucli sigui recent (versi\u00f3 del kernel) i que el maquinari sigui compatible",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vols comen\u00e7ar la configuraci\u00f3?"
+ }
+ }
+ },
+ "title": "Comprovador de font d'alimentaci\u00f3 de Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/cs.json b/homeassistant/components/rpi_power/translations/cs.json
new file mode 100644
index 00000000000000..b60cb60f985ead
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/cs.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nelze naj\u00edt t\u0159\u00eddu syst\u00e9mu pot\u0159ebnou pro tuto komponentu, ujist\u011bte se, \u017ee je va\u0161e j\u00e1dro aktu\u00e1ln\u00ed a hardware podporov\u00e1n",
+ "single_instance_allowed": "Ji\u017e je nakonfigurov\u00e1no.Je mo\u017en\u00e1 pouze jedna konfigurace."
+ },
+ "step": {
+ "confirm": {
+ "description": "Chcete zah\u00e1jit nastaven\u00ed?"
+ }
+ }
+ },
+ "title": "Kontrola nap\u00e1jec\u00edho zdroje Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/el.json b/homeassistant/components/rpi_power/translations/el.json
new file mode 100644
index 00000000000000..89042f53e884b2
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/el.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03bb\u03ac\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u03c0\u03c5\u03c1\u03ae\u03bd\u03b1\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9",
+ "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."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7;"
+ }
+ }
+ },
+ "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c3\u03af\u03b1\u03c2 Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/en.json b/homeassistant/components/rpi_power/translations/en.json
new file mode 100644
index 00000000000000..6995190979acb4
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/en.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to start set up?"
+ }
+ }
+ },
+ "title": "Raspberry Pi Power Supply Checker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/es.json b/homeassistant/components/rpi_power/translations/es.json
new file mode 100644
index 00000000000000..215b15014abc9d
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/es.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y el hardware es compatible",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?"
+ }
+ }
+ },
+ "title": "Comprobador de fuente de alimentaci\u00f3n de Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/et.json b/homeassistant/components/rpi_power/translations/et.json
new file mode 100644
index 00000000000000..350e09ca86ff27
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/et.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veenduge, et teie kernel on v\u00e4rske ja riistvara on toetatud",
+ "single_instance_allowed": "Seadistused on juba tehtud. Korraga saab olla ainult \u00fcks konfiguratsioon."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kas alustame paigaldusega?"
+ }
+ }
+ },
+ "title": "Raspberry Pi toiteallika kontroll"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/fr.json b/homeassistant/components/rpi_power/translations/fr.json
new file mode 100644
index 00000000000000..7e4fd715ee0e1d
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/fr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Impossible de trouver la classe syst\u00e8me n\u00e9cessaire pour ce composant, assurez-vous que votre noyau est r\u00e9cent et que le mat\u00e9riel est pris en charge",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ },
+ "step": {
+ "confirm": {
+ "description": "Voulez-vous commencer la configuration ?"
+ }
+ }
+ },
+ "title": "V\u00e9rificateur d'alimentation Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/it.json b/homeassistant/components/rpi_power/translations/it.json
new file mode 100644
index 00000000000000..4e7a14d05e844b
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/it.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Impossibile trovare la classe di sistema necessaria per questo componente, assicurarsi che il kernel sia recente e che l'hardware sia supportato",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vuoi iniziare la configurazione?"
+ }
+ }
+ },
+ "title": "Controllo alimentazione Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json
new file mode 100644
index 00000000000000..022718332203b2
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/ko.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.",
+ "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
+ },
+ "step": {
+ "confirm": {
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ },
+ "title": "\ub77c\uc988\ubca0\ub9ac\ud30c\uc774 \uc804\uc6d0 \uacf5\uae09 \uc7a5\uce58 \uac80\uc0ac\uae30"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/lb.json b/homeassistant/components/rpi_power/translations/lb.json
new file mode 100644
index 00000000000000..1b4c77f4fda928
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/lb.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Kann d\u00e9i Systemklass fir d\u00ebs noutwendeg Komponent net fannen, stell s\u00e9cher dass de Kernel rezent ass an d'Hardware \u00ebnnerst\u00ebtzt g\u00ebtt."
+ }
+ },
+ "title": "Raspberry Pi Netzdeel Checker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json
new file mode 100644
index 00000000000000..a18ff63733ea1c
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/nl.json
@@ -0,0 +1,3 @@
+{
+ "title": "Raspberry Pi Voeding Checker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/no.json b/homeassistant/components/rpi_power/translations/no.json
new file mode 100644
index 00000000000000..63f46667e79baf
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/no.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Finner ikke systemklassen som trengs for denne komponenten, s\u00f8rg for at kjernen din er ny og at maskinvaren st\u00f8ttes",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vil du starte oppsettet?"
+ }
+ }
+ },
+ "title": "Raspberry Pi str\u00f8mforsyningskontroll"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/pl.json b/homeassistant/components/rpi_power/translations/pl.json
new file mode 100644
index 00000000000000..c9599d7182b20a
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/pl.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
+ }
+ }
+ },
+ "title": "Sprawdzanie zasilacza Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/ru.json b/homeassistant/components/rpi_power/translations/ru.json
new file mode 100644
index 00000000000000..f91df15e1b31d1
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/ru.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443 \u0412\u0430\u0441 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u043e\u0432\u0435\u0439\u0448\u0435\u0435 \u044f\u0434\u0440\u043e \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435.",
+ "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": {
+ "confirm": {
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?"
+ }
+ }
+ },
+ "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u043f\u0438\u0442\u0430\u043d\u0438\u044f Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/zh-Hant.json b/homeassistant/components/rpi_power/translations/zh-Hant.json
new file mode 100644
index 00000000000000..37dbb151d8e071
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/zh-Hant.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u627e\u4e0d\u5230\u7cfb\u7d71\u6240\u9700\u7684\u5143\u4ef6\uff0c\u8acb\u78ba\u5b9a Kernel \u70ba\u6700\u65b0\u7248\u672c\u3001\u540c\u6642\u786c\u9ad4\u70ba\u652f\u63f4\u72c0\u614b",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f"
+ }
+ }
+ },
+ "title": "Raspberry Pi \u96fb\u6e90\u4f9b\u61c9\u6aa2\u67e5\u5de5\u5177"
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py
index ee761696cc0802..dc8eb862ff783e 100644
--- a/homeassistant/components/samsungtv/bridge.py
+++ b/homeassistant/components/samsungtv/bridge.py
@@ -22,7 +22,7 @@
LOGGER,
METHOD_LEGACY,
RESULT_AUTH_MISSING,
- RESULT_NOT_SUCCESSFUL,
+ RESULT_CANNOT_CONNECT,
RESULT_NOT_SUPPORTED,
RESULT_SUCCESS,
VALUE_CONF_ID,
@@ -164,7 +164,7 @@ def try_connect(self):
return RESULT_NOT_SUPPORTED
except OSError as err:
LOGGER.debug("Failing config: %s, error: %s", config, err)
- return RESULT_NOT_SUCCESSFUL
+ return RESULT_CANNOT_CONNECT
def _get_remote(self):
"""Create or return a remote control instance."""
@@ -232,7 +232,7 @@ def try_connect(self):
if result:
return result
- return RESULT_NOT_SUCCESSFUL
+ return RESULT_CANNOT_CONNECT
def _send_key(self, key):
"""Send the key using websocket protocol."""
diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py
index 9c18600670c606..7a7fa26f922584 100644
--- a/homeassistant/components/samsungtv/config_flow.py
+++ b/homeassistant/components/samsungtv/config_flow.py
@@ -31,7 +31,7 @@
METHOD_LEGACY,
METHOD_WEBSOCKET,
RESULT_AUTH_MISSING,
- RESULT_NOT_SUCCESSFUL,
+ RESULT_CANNOT_CONNECT,
RESULT_SUCCESS,
)
@@ -87,10 +87,10 @@ def _try_connect(self):
for method in SUPPORTED_METHODS:
self._bridge = SamsungTVBridge.get_bridge(method, self._host)
result = self._bridge.try_connect()
- if result != RESULT_NOT_SUCCESSFUL:
+ if result != RESULT_CANNOT_CONNECT:
return result
LOGGER.debug("No working config found")
- return RESULT_NOT_SUCCESSFUL
+ return RESULT_CANNOT_CONNECT
async def async_step_import(self, user_input=None):
"""Handle configuration by yaml file."""
diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py
index c08f07e637945d..e043c74b34768f 100644
--- a/homeassistant/components/samsungtv/const.py
+++ b/homeassistant/components/samsungtv/const.py
@@ -16,7 +16,7 @@
RESULT_AUTH_MISSING = "auth_missing"
RESULT_SUCCESS = "success"
-RESULT_NOT_SUCCESSFUL = "not_successful"
+RESULT_CANNOT_CONNECT = "cannot_connect"
RESULT_NOT_SUPPORTED = "not_supported"
METHOD_LEGACY = "legacy"
diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json
index e615728776ebba..b326f2ab548943 100644
--- a/homeassistant/components/samsungtv/strings.json
+++ b/homeassistant/components/samsungtv/strings.json
@@ -6,7 +6,7 @@
"description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
- "name": "Name"
+ "name": "[%key:common::config_flow::data::name%]"
}
},
"confirm": {
@@ -15,11 +15,11 @@
}
},
"abort": {
- "already_in_progress": "Samsung TV configuration is already in progress.",
- "already_configured": "This Samsung TV is already configured.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.",
- "not_successful": "Unable to connect to this Samsung TV device.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_supported": "This Samsung TV device is currently not supported."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json
index 5a5fe7e7986da7..9b0b9a37844f17 100644
--- a/homeassistant/components/samsungtv/translations/ca.json
+++ b/homeassistant/components/samsungtv/translations/ca.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Aquest televisor Samsung ja configurat.",
- "already_in_progress": "La configuraci\u00f3 del televisor Samsung ja est\u00e0 en curs.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"not_successful": "No s'ha pogut connectar amb el dispositiu el televisor Samsung.",
"not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible."
},
diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json
index 35510858c551b1..ff33876b93af76 100644
--- a/homeassistant/components/samsungtv/translations/en.json
+++ b/homeassistant/components/samsungtv/translations/en.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "This Samsung TV is already configured.",
- "already_in_progress": "Samsung TV configuration is already in progress.",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.",
+ "cannot_connect": "Failed to connect",
"not_successful": "Unable to connect to this Samsung TV device.",
"not_supported": "This Samsung TV device is currently not supported."
},
diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json
index 308df08de0d7af..10fd0cf358ac0a 100644
--- a/homeassistant/components/samsungtv/translations/es.json
+++ b/homeassistant/components/samsungtv/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "Este televisor Samsung ya est\u00e1 configurado.",
"already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en marcha.",
"auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.",
+ "cannot_connect": "No se pudo conectar",
"not_successful": "No se puede conectar a este dispositivo Samsung TV.",
"not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible."
},
diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json
new file mode 100644
index 00000000000000..d71a861b6b5659
--- /dev/null
+++ b/homeassistant/components/samsungtv/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json
index 4236b1d8ed8de0..4abbcce70c06e1 100644
--- a/homeassistant/components/samsungtv/translations/it.json
+++ b/homeassistant/components/samsungtv/translations/it.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.",
- "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.",
+ "cannot_connect": "Impossibile connettersi",
"not_successful": "Impossibile connettersi a questo dispositivo Samsung TV.",
"not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato."
},
diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json
index e0420ba74af86e..dd0b63581c07e3 100644
--- a/homeassistant/components/samsungtv/translations/no.json
+++ b/homeassistant/components/samsungtv/translations/no.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Denne Samsung TV-en er allerede konfigurert.",
- "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"auth_missing": "Home Assistant er ikke godkjent til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 godkjenne Home Assistent.",
+ "cannot_connect": "Tilkobling mislyktes.",
"not_successful": "Kan ikke koble til denne Samsung TV-enheten.",
"not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke."
},
diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json
index c5ee5e8348c86d..8af4f95404cf5c 100644
--- a/homeassistant/components/samsungtv/translations/ru.json
+++ b/homeassistant/components/samsungtv/translations/ru.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"not_successful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.",
"not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f."
},
diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json
index 973b84dd2eed3a..1b0b0a09ec4a23 100644
--- a/homeassistant/components/samsungtv/translations/zh-Hant.json
+++ b/homeassistant/components/samsungtv/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u4e09\u661f\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u4e09\u661f\u96fb\u8996\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002",
"not_successful": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e09\u661f\u96fb\u8996\u8a2d\u5099\u3002",
"not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002"
diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py
index 19763903f27845..ea9c19376f694b 100644
--- a/homeassistant/components/satel_integra/binary_sensor.py
+++ b/homeassistant/components/satel_integra/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for Satel Integra zone states- represented as binary sensors."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_SMOKE,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -89,7 +92,7 @@ def name(self):
@property
def icon(self):
"""Icon for device by its type."""
- if self._zone_type == "smoke":
+ if self._zone_type == DEVICE_CLASS_SMOKE:
return "mdi:fire"
@property
diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py
index 6e072f7377e35b..e116fb0e8611ee 100644
--- a/homeassistant/components/season/sensor.py
+++ b/homeassistant/components/season/sensor.py
@@ -10,7 +10,7 @@
from homeassistant.const import CONF_NAME, CONF_TYPE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-import homeassistant.util.dt as dt_util
+from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hemisphere = EQUATOR
_LOGGER.debug(_type)
- add_entities([Season(hass, hemisphere, _type, name)])
+ add_entities([Season(hass, hemisphere, _type, name)], True)
return True
@@ -117,9 +117,9 @@ def __init__(self, hass, hemisphere, season_tracking_type, name):
self.hass = hass
self._name = name
self.hemisphere = hemisphere
- self.datetime = dt_util.utcnow().replace(tzinfo=None)
+ self.datetime = None
self.type = season_tracking_type
- self.season = get_season(self.datetime, self.hemisphere, self.type)
+ self.season = None
@property
def name(self):
@@ -143,5 +143,5 @@ def icon(self):
def update(self):
"""Update season."""
- self.datetime = dt_util.utcnow().replace(tzinfo=None)
+ self.datetime = utcnow().replace(tzinfo=None)
self.season = get_season(self.datetime, self.hemisphere, self.type)
diff --git a/homeassistant/components/season/translations/sensor.et.json b/homeassistant/components/season/translations/sensor.et.json
new file mode 100644
index 00000000000000..eb9953a73ce816
--- /dev/null
+++ b/homeassistant/components/season/translations/sensor.et.json
@@ -0,0 +1,16 @@
+{
+ "state": {
+ "season__season": {
+ "autumn": "S\u00fcgis",
+ "spring": "Kevad",
+ "summer": "Suvi",
+ "winter": "Talv"
+ },
+ "season__season__": {
+ "autumn": "S\u00fcgis",
+ "spring": "Kevad",
+ "summer": "Suvi",
+ "winter": "Talv"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py
index 6dbf4d5c2b73a1..8d8907f2dcf262 100644
--- a/homeassistant/components/sendgrid/notify.py
+++ b/homeassistant/components/sendgrid/notify.py
@@ -15,6 +15,7 @@
CONF_RECIPIENT,
CONF_SENDER,
CONTENT_TYPE_TEXT_PLAIN,
+ HTTP_ACCEPTED,
)
import homeassistant.helpers.config_validation as cv
@@ -65,5 +66,5 @@ def send_message(self, message="", **kwargs):
}
response = self._sg.client.mail.send.post(request_body=data)
- if response.status_code != 202:
+ if response.status_code != HTTP_ACCEPTED:
_LOGGER.error("Unable to send notification")
diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json
index 1363d44d89f9b5..44a45f07ce92b0 100644
--- a/homeassistant/components/sense/strings.json
+++ b/homeassistant/components/sense/strings.json
@@ -10,12 +10,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/sense/translations/ca.json b/homeassistant/components/sense/translations/ca.json
index 4b1035607d606f..096b4419dae7ad 100644
--- a/homeassistant/components/sense/translations/ca.json
+++ b/homeassistant/components/sense/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
diff --git a/homeassistant/components/sense/translations/en.json b/homeassistant/components/sense/translations/en.json
index 181dadea06c2f9..5582a8424a6a2a 100644
--- a/homeassistant/components/sense/translations/en.json
+++ b/homeassistant/components/sense/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json
index 4e1ddd01b42f9c..277e2e1539b95a 100644
--- a/homeassistant/components/sense/translations/it.json
+++ b/homeassistant/components/sense/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/sense/translations/no.json b/homeassistant/components/sense/translations/no.json
index c3457ccb280474..ca987279bb783a 100644
--- a/homeassistant/components/sense/translations/no.json
+++ b/homeassistant/components/sense/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json
index c32b61e30ad9d1..7cf2ebe47099b2 100644
--- a/homeassistant/components/sense/translations/pl.json
+++ b/homeassistant/components/sense/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/sense/translations/ru.json b/homeassistant/components/sense/translations/ru.json
index 163af6cb512aaa..74be3049a750be 100644
--- a/homeassistant/components/sense/translations/ru.json
+++ b/homeassistant/components/sense/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json
index e989026347764d..356e58f640b9f7 100644
--- a/homeassistant/components/sense/translations/zh-Hant.json
+++ b/homeassistant/components/sense/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py
new file mode 100644
index 00000000000000..4741f8a3b548cd
--- /dev/null
+++ b/homeassistant/components/sensor/group.py
@@ -0,0 +1,14 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.exclude_domain()
diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json
index 8238a8b6ab095d..450f5b60537c93 100644
--- a/homeassistant/components/sensor/translations/et.json
+++ b/homeassistant/components/sensor/translations/et.json
@@ -1,4 +1,36 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "Praegune {entity_name} aku tase",
+ "is_current": "Praegune {entity_name} voolutugevus",
+ "is_energy": "Praegune {entity_name} v\u00f5imsus",
+ "is_humidity": "Praegune {entity_name} niiskus",
+ "is_illuminance": "Praegune {entity_name} valgustatus",
+ "is_power": "Praegune {entity_name} toide (v\u00f5imsus)",
+ "is_power_factor": "Praegune {entity_name} v\u00f5imsusfaktor",
+ "is_pressure": "Praegune {entity_name} r\u00f5hk",
+ "is_signal_strength": "Praegune {entity_name} signaali tugevus",
+ "is_temperature": "Praegune {entity_name} temperatuur",
+ "is_timestamp": "Praegune {entity_name} aeg",
+ "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus",
+ "is_voltage": "Praegune {entity_name}pinge"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} aku tase muutub",
+ "current": "{entity_name} voolutugevus muutub",
+ "energy": "{entity_name} v\u00f5imsus muutub",
+ "humidity": "{entity_name} niiskus muutub",
+ "illuminance": "{entity_name} valgustustugevus muutub",
+ "power": "{entity_name} energiare\u017eiimi muutub",
+ "power_factor": "{entity_name} v\u00f5imsus muutub",
+ "pressure": "{entity_name} r\u00f5hk muutub",
+ "signal_strength": "{entity_name} signaalitugevus muutub",
+ "temperature": "{entity_name} temperatuur muutub",
+ "timestamp": "{entity_name} aeg muutub",
+ "value": "{entity_name} v\u00e4\u00e4rtus muutub",
+ "voltage": "{entity_name} pingemuutub"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json
index 84c32d8dc9a006..6e5d50c1caeedf 100644
--- a/homeassistant/components/sensor/translations/pl.json
+++ b/homeassistant/components/sensor/translations/pl.json
@@ -2,14 +2,18 @@
"device_automation": {
"condition_type": {
"is_battery_level": "obecny poziom na\u0142adowania baterii {entity_name}",
+ "is_current": "Bie\u017c\u0105cy pr\u0105d {entity_name}",
+ "is_energy": "Bie\u017c\u0105ca energia {entity_name}",
"is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}",
"is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}",
"is_power": "obecna moc {entity_name}",
+ "is_power_factor": "Bie\u017c\u0105cy wsp\u00f3\u0142czynnik mocy {entity_name}",
"is_pressure": "obecne ci\u015bnienie {entity_name}",
"is_signal_strength": "obecna si\u0142a sygna\u0142u {entity_name}",
"is_temperature": "obecna temperatura {entity_name}",
"is_timestamp": "obecny znacznik czasu {entity_name}",
- "is_value": "obecna warto\u015b\u0107 {entity_name}"
+ "is_value": "obecna warto\u015b\u0107 {entity_name}",
+ "is_voltage": "Bie\u017c\u0105ce napi\u0119cie {entity_name}"
},
"trigger_type": {
"battery_level": "zmieni si\u0119 poziom baterii {entity_name}",
diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json
index 56e587bb44c568..391415409f5bd1 100644
--- a/homeassistant/components/sensor/translations/uk.json
+++ b/homeassistant/components/sensor/translations/uk.json
@@ -1,4 +1,9 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "\u041f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0437\u0430\u0440\u044f\u0434\u0443 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430 {entity_name}"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index 1a9bb74a8ee7a7..2bcb38ed168ac5 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -3,6 +3,6 @@
"name": "Sentry",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sentry",
- "requirements": ["sentry-sdk==0.17.4"],
+ "requirements": ["sentry-sdk==0.18.0"],
"codeowners": ["@dcramer", "@frenck"]
}
diff --git a/homeassistant/components/sentry/strings.json b/homeassistant/components/sentry/strings.json
index 593a8c5c8d07ad..71196d52f8d864 100644
--- a/homeassistant/components/sentry/strings.json
+++ b/homeassistant/components/sentry/strings.json
@@ -4,10 +4,15 @@
"user": {
"title": "Sentry",
"description": "Enter your Sentry DSN",
- "data": { "dsn": "DSN" }
+ "data": {
+ "dsn": "DSN"
+ }
}
},
- "error": { "unknown": "Unexpected error", "bad_dsn": "Invalid DSN" },
+ "error": {
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "bad_dsn": "Invalid DSN"
+ },
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json
index 5d6e27bd73786a..6e6d640cd452d5 100644
--- a/homeassistant/components/sentry/translations/de.json
+++ b/homeassistant/components/sentry/translations/de.json
@@ -9,6 +9,9 @@
},
"step": {
"user": {
+ "data": {
+ "dsn": "DSN"
+ },
"description": "Gib deine Sentry-DSN ein",
"title": "Sentry"
}
diff --git a/homeassistant/components/sentry/translations/el.json b/homeassistant/components/sentry/translations/el.json
new file mode 100644
index 00000000000000..a1c27233094890
--- /dev/null
+++ b/homeassistant/components/sentry/translations/el.json
@@ -0,0 +1,16 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "event_handled": "\u03a3\u03c4\u03b5\u03af\u03bb\u03c4\u03b5 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1",
+ "event_third_party_packages": "\u03a3\u03c4\u03b5\u03af\u03bb\u03c4\u03b5 \u03b5\u03ba\u03b4\u03b7\u03bb\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c0\u03b1\u03ba\u03ad\u03c4\u03b1 \u03c4\u03c1\u03af\u03c4\u03c9\u03bd",
+ "logging_event_level": "\u03a4\u03bf \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 Sentry \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03b3\u03b9\u03b1",
+ "logging_level": "\u03a4\u03bf \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 Sentry \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c9\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac \u03b3\u03b9\u03b1",
+ "tracing": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b5\u03c0\u03b9\u03b4\u03cc\u03c3\u03b5\u03c9\u03bd",
+ "tracing_sample_rate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03af\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2. \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0,0 \u03ba\u03b1\u03b9 1,0 (1,0 = 100%)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/et.json b/homeassistant/components/sentry/translations/et.json
new file mode 100644
index 00000000000000..255510c130ca95
--- /dev/null
+++ b/homeassistant/components/sentry/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json
index 7e60891e166070..f3adbefa3d71cb 100644
--- a/homeassistant/components/sentry/translations/ko.json
+++ b/homeassistant/components/sentry/translations/ko.json
@@ -13,5 +13,21 @@
"title": "Sentry"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "\ud658\uacbd\uc758 \uc120\ud0dd\uc801 \uba85\uce6d",
+ "event_custom_components": "\uc0ac\uc6a9\uc790 \uc9c0\uc815 \uad6c\uc131 \uc694\uc18c\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
+ "event_handled": "\ucc98\ub9ac\ub41c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
+ "event_third_party_packages": "\uc368\ub4dc\ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
+ "logging_event_level": "Log level Sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4.",
+ "logging_level": "Log level sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc \ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4.",
+ "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654",
+ "tracing_sample_rate": "\uc0d8\ud50c\ub9c1 \uc18d\ub3c4 \ucd94\uc801; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100 %)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json
index fa87b8510c748b..5537d8d8ede96a 100644
--- a/homeassistant/components/sentry/translations/pl.json
+++ b/homeassistant/components/sentry/translations/pl.json
@@ -6,13 +6,29 @@
},
"error": {
"bad_dsn": "Nieprawid\u0142owy DSN",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
+ "data": {
+ "dsn": "DSN"
+ },
"description": "Wprowad\u017a DSN Sentry",
"title": "Sentry"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "Opcjonalna nazwa \u015brodowiska.",
+ "event_custom_components": "Wysy\u0142aj zdarzenia z niestandardowych komponent\u00f3w",
+ "logging_event_level": "Poziom wpis\u00f3w dziennika, dla kt\u00f3rego Sentry zarejestruje zdarzenie",
+ "tracing": "W\u0142\u0105cz \u015bledzenie wydajno\u015bci",
+ "tracing_sample_rate": "Cz\u0119stotliwo\u015b\u0107 \u015bledzenia; od 0.0 do 1.0 (1.0 = 100%)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py
index a2d205de240981..0ad3d87a1715f0 100644
--- a/homeassistant/components/sesame/lock.py
+++ b/homeassistant/components/sesame/lock.py
@@ -88,8 +88,8 @@ def update(self) -> None:
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes."""
- attributes = {}
- attributes[ATTR_DEVICE_ID] = self._device_id
- attributes[ATTR_SERIAL_NO] = self._serial
- attributes[ATTR_BATTERY_LEVEL] = self._battery
- return attributes
+ return {
+ ATTR_DEVICE_ID: self._device_id,
+ ATTR_SERIAL_NO: self._serial,
+ ATTR_BATTERY_LEVEL: self._battery,
+ }
diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py
index 42b198d48d98e5..7abed897346253 100644
--- a/homeassistant/components/seventeentrack/sensor.py
+++ b/homeassistant/components/seventeentrack/sensor.py
@@ -16,6 +16,7 @@
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_call_later
from homeassistant.util import Throttle, slugify
_LOGGER = logging.getLogger(__name__)
@@ -220,7 +221,8 @@ async def async_update(self):
await self._data.async_update()
if not self.available:
- self.hass.async_create_task(self._remove())
+ # Entity cannot be removed while its being added
+ async_call_later(self.hass, 1, self._remove)
return
package = self._data.packages.get(self._tracking_number, None)
@@ -229,7 +231,8 @@ async def async_update(self):
# delivered, post a notification:
if package.status == VALUE_DELIVERED and not self._data.show_delivered:
self._notify_delivered()
- self.hass.async_create_task(self._remove())
+ # Entity cannot be removed while its being added
+ async_call_later(self.hass, 1, self._remove)
return
self._attrs.update(
@@ -238,7 +241,7 @@ async def async_update(self):
self._state = package.status
self._friendly_name = package.friendly_name
- async def _remove(self):
+ async def _remove(self, *_):
"""Remove entity itself."""
await self.async_remove()
diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json
index 114087697ad1af..ccd66a3829715b 100644
--- a/homeassistant/components/sharkiq/strings.json
+++ b/homeassistant/components/sharkiq/strings.json
@@ -22,7 +22,7 @@
"abort": {
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json
new file mode 100644
index 00000000000000..5a1d4f2f185183
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/de.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/el.json b/homeassistant/components/sharkiq/translations/el.json
new file mode 100644
index 00000000000000..4c6777955253d3
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/el.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/et.json b/homeassistant/components/sharkiq/translations/et.json
new file mode 100644
index 00000000000000..ff5b447a315a7b
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/hu.json b/homeassistant/components/sharkiq/translations/hu.json
new file mode 100644
index 00000000000000..9f2fd5d72f4336
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/ko.json b/homeassistant/components/sharkiq/translations/ko.json
new file mode 100644
index 00000000000000..d7e196c09fb277
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/ko.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\uc554\ud638",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\uc554\ud638",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/lb.json b/homeassistant/components/sharkiq/translations/lb.json
new file mode 100644
index 00000000000000..1da09e6b8ecd7a
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/lb.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json
new file mode 100644
index 00000000000000..03605190c3ced1
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/nl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Paswoord",
+ "username": "Gebruikersnaam"
+ }
+ },
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json
index dcb12e8690653d..283e2c4f440a48 100644
--- a/homeassistant/components/sharkiq/translations/pl.json
+++ b/homeassistant/components/sharkiq/translations/pl.json
@@ -1,12 +1,15 @@
{
"config": {
"abort": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "already_configured_account": "Konto jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"reauth_successful": "Token dost\u0119pu zosta\u0142 pomy\u015blnie zaktualizowany",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/sharkiq/translations/sv.json b/homeassistant/components/sharkiq/translations/sv.json
new file mode 100644
index 00000000000000..75f4175c9af45a
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "Ov\u00e4ntat fel"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "L\u00f6senord",
+ "username": "Anv\u00e4ndarnamn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index 83d5d7b9f3afcc..c63858f06523e2 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
temperature_unit,
)
try:
- async with async_timeout.timeout(5):
+ async with async_timeout.timeout(10):
device = await aioshelly.Device.create(
aiohttp_client.async_get_clientsession(hass),
options,
@@ -86,7 +86,7 @@ async def _async_update_data(self):
try:
async with async_timeout.timeout(5):
return await self.device.update()
- except aiocoap_error.Error as err:
+ except (aiocoap_error.Error, OSError) as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err
@property
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index c9a13249aa8688..1460c62f153d1c 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -20,31 +20,31 @@
name="Overheating", device_class=DEVICE_CLASS_PROBLEM
),
("device", "overpower"): BlockAttributeDescription(
- name="Over Power", device_class=DEVICE_CLASS_PROBLEM
+ name="Overpowering", device_class=DEVICE_CLASS_PROBLEM
),
("light", "overpower"): BlockAttributeDescription(
- name="Over Power", device_class=DEVICE_CLASS_PROBLEM
+ name="Overpowering", device_class=DEVICE_CLASS_PROBLEM
),
("relay", "overpower"): BlockAttributeDescription(
- name="Over Power", device_class=DEVICE_CLASS_PROBLEM
+ name="Overpowering", device_class=DEVICE_CLASS_PROBLEM
),
("sensor", "dwIsOpened"): BlockAttributeDescription(
name="Door", device_class=DEVICE_CLASS_OPENING
),
("sensor", "flood"): BlockAttributeDescription(
- name="flood", device_class=DEVICE_CLASS_MOISTURE
+ name="Flood", device_class=DEVICE_CLASS_MOISTURE
),
("sensor", "gas"): BlockAttributeDescription(
- name="gas",
+ name="Gas",
device_class=DEVICE_CLASS_GAS,
value=lambda value: value in ["mild", "heavy"],
device_state_attributes=lambda block: {"detected": block.gas},
),
("sensor", "smoke"): BlockAttributeDescription(
- name="smoke", device_class=DEVICE_CLASS_SMOKE
+ name="Smoke", device_class=DEVICE_CLASS_SMOKE
),
("sensor", "vibration"): BlockAttributeDescription(
- name="vibration", device_class=DEVICE_CLASS_VIBRATION
+ name="Vibration", device_class=DEVICE_CLASS_VIBRATION
),
}
diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py
index 6446d2dd2d2e59..b13c4090a1095a 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -8,7 +8,12 @@
import voluptuous as vol
from homeassistant import config_entries, core
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN # pylint:disable=unused-import
@@ -57,6 +62,8 @@ async def async_step_user(self, user_input=None):
info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS:
errors["base"] = "cannot_connect"
+ except aioshelly.FirmwareUnsupported:
+ return self.async_abort(reason="unsupported_firmware")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -91,7 +98,7 @@ async def async_step_credentials(self, user_input=None):
try:
device_info = await validate_input(self.hass, self.host, user_input)
except aiohttp.ClientResponseError as error:
- if error.status == 401:
+ if error.status == HTTP_UNAUTHORIZED:
errors["base"] = "invalid_auth"
else:
errors["base"] = "cannot_connect"
@@ -128,6 +135,8 @@ async def async_step_zeroconf(self, zeroconf_info):
self.info = info = await self._async_get_info(zeroconf_info["host"])
except HTTP_CONNECT_ERRORS:
return self.async_abort(reason="cannot_connect")
+ except aioshelly.FirmwareUnsupported:
+ return self.async_abort(reason="unsupported_firmware")
await self.async_set_unique_id(info["mac"])
self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]})
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index b1a61bbbf59e70..237deec4da1460 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -20,6 +20,47 @@ def temperature_unit(block_info: dict) -> str:
return TEMP_CELSIUS
+def shelly_naming(self, block, entity_type: str):
+ """Naming for switch and sensors."""
+
+ entity_name = self.wrapper.name
+ if not block:
+ return f"{entity_name} {self.description.name}"
+
+ channels = 0
+ mode = "relays"
+ if "num_outputs" in self.wrapper.device.shelly:
+ channels = self.wrapper.device.shelly["num_outputs"]
+ if (
+ self.wrapper.model in ["SHSW-21", "SHSW-25"]
+ and self.wrapper.device.settings["mode"] == "roller"
+ ):
+ channels = 1
+ if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly:
+ channels = self.wrapper.device.shelly["num_emeters"]
+ mode = "emeters"
+ if channels > 1 and block.type != "device":
+ # Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release
+ if "name" in self.wrapper.device.settings[mode][int(block.channel)]:
+ entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"]
+ else:
+ entity_name = None
+ if not entity_name:
+ if self.wrapper.model == "SHEM-3":
+ base = ord("A")
+ else:
+ base = ord("1")
+ entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}"
+
+ if entity_type == "switch":
+ return entity_name
+
+ if entity_type == "sensor":
+ return f"{entity_name} {self.description.name}"
+
+ raise ValueError
+
+
async def async_setup_entry_attribute_entities(
hass, config_entry, async_add_entities, sensors, sensor_class
):
@@ -42,11 +83,11 @@ async def async_setup_entry_attribute_entities(
if not blocks:
return
- counts = Counter([item[0].type for item in blocks])
+ counts = Counter([item[1] for item in blocks])
async_add_entities(
[
- sensor_class(wrapper, block, sensor_id, description, counts[block.type])
+ sensor_class(wrapper, block, sensor_id, description, counts[sensor_id])
for block, sensor_id, description in blocks
]
)
@@ -75,7 +116,7 @@ def __init__(self, wrapper: ShellyDeviceWrapper, block):
"""Initialize Shelly entity."""
self.wrapper = wrapper
self.block = block
- self._name = f"{self.wrapper.name} {self.block.description.replace('_', ' ')}"
+ self._name = shelly_naming(self, block, "switch")
@property
def name(self):
@@ -142,13 +183,7 @@ def __init__(
self._unit = unit
self._unique_id = f"{super().unique_id}-{self.attribute}"
-
- name_parts = [self.wrapper.name]
- if same_type_count > 1:
- name_parts.append(str(block.channel))
- name_parts.append(self.description.name)
-
- self._name = " ".join(name_parts)
+ self._name = shelly_naming(self, block, "sensor")
@property
def unique_id(self):
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index 38ccc9e0f74934..3816d31222eae4 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==0.3.1"],
- "zeroconf": [{"type": "_http._tcp.local.", "name":"shelly*"}],
+ "requirements": ["aioshelly==0.3.4"],
+ "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu"]
}
diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index 8a24a6380ed803..e82f167ca6055e 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -5,8 +5,10 @@
DEGREE,
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ VOLT,
)
from .entity import (
@@ -52,12 +54,30 @@
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_POWER,
),
+ ("emeter", "voltage"): BlockAttributeDescription(
+ name="Voltage",
+ unit=VOLT,
+ value=lambda value: round(value, 1),
+ device_class=sensor.DEVICE_CLASS_VOLTAGE,
+ ),
+ ("emeter", "powerFactor"): BlockAttributeDescription(
+ name="Power Factor",
+ unit=PERCENTAGE,
+ value=lambda value: round(value * 100, 1),
+ device_class=sensor.DEVICE_CLASS_POWER_FACTOR,
+ ),
("relay", "power"): BlockAttributeDescription(
name="Power",
unit=POWER_WATT,
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_POWER,
),
+ ("roller", "rollerPower"): BlockAttributeDescription(
+ name="Power",
+ unit=POWER_WATT,
+ value=lambda value: round(value, 1),
+ device_class=sensor.DEVICE_CLASS_POWER,
+ ),
("device", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,
@@ -70,6 +90,12 @@
value=lambda value: round(value / 1000, 2),
device_class=sensor.DEVICE_CLASS_ENERGY,
),
+ ("emeter", "energyReturned"): BlockAttributeDescription(
+ name="Energy Returned",
+ unit=ENERGY_KILO_WATT_HOUR,
+ value=lambda value: round(value / 1000, 2),
+ device_class=sensor.DEVICE_CLASS_ENERGY,
+ ),
("light", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,
@@ -83,6 +109,12 @@
value=lambda value: round(value / 60 / 1000, 2),
device_class=sensor.DEVICE_CLASS_ENERGY,
),
+ ("roller", "rollerEnergy"): BlockAttributeDescription(
+ name="Energy",
+ unit=ENERGY_KILO_WATT_HOUR,
+ value=lambda value: round(value / 60 / 1000, 2),
+ device_class=sensor.DEVICE_CLASS_ENERGY,
+ ),
("sensor", "concentration"): BlockAttributeDescription(
name="Gas Concentration",
unit=CONCENTRATION_PARTS_PER_MILLION,
@@ -104,7 +136,7 @@
),
("sensor", "luminosity"): BlockAttributeDescription(
name="Luminosity",
- unit="lx",
+ unit=LIGHT_LUX,
device_class=sensor.DEVICE_CLASS_ILLUMINANCE,
),
("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE),
diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json
index 16dc331e4523f8..69a0920422735a 100644
--- a/homeassistant/components/shelly/strings.json
+++ b/homeassistant/components/shelly/strings.json
@@ -4,6 +4,7 @@
"flow_title": "Shelly: {name}",
"step": {
"user": {
+ "description": "Before set up, the battery-powered device must be woken up by pressing the button on the device.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
@@ -15,7 +16,7 @@
}
},
"confirm_discovery": {
- "description": "Do you want to set up the {model} at {host}?"
+ "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, the battery-powered device must be woken up by pressing the button on the device."
}
},
"error": {
@@ -24,7 +25,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "unsupported_firmware": "The device is using an unsupported firmware version."
}
}
}
diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py
index 0aaa6dbc911bee..0dcefb51cf991b 100644
--- a/homeassistant/components/shelly/switch.py
+++ b/homeassistant/components/shelly/switch.py
@@ -14,7 +14,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
wrapper = hass.data[DOMAIN][config_entry.entry_id]
# In roller mode the relay blocks exist but do not contain required info
- if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay":
+ if (
+ wrapper.model in ["SHSW-21", "SHSW-25"]
+ and wrapper.device.settings["mode"] != "relay"
+ ):
return
relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"]
diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json
index 7e8c3873c11141..41ffcb34b89f1c 100644
--- a/homeassistant/components/shelly/translations/ca.json
+++ b/homeassistant/components/shelly/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "El dispositiu ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "unsupported_firmware": "El dispositiu utilitza una versi\u00f3 de programari no compatible."
},
"error": {
"auth_not_supported": "Actualment els dispositius Shelly amb autenticaci\u00f3 no son compatibles.",
@@ -12,7 +13,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Voleu configurar {model} a {host}?"
+ "description": "Vols configurar {model} a {host}? \n\nAbans de configurar-lo, el dispositiu amb bateria ha d'estar despert, prem el bot\u00f3 del dispositiu per despertar-lo."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Amfitri\u00f3"
- }
+ },
+ "description": "Abans de configurar-lo, el dispositiu amb bateria ha d'estar despert, prem el bot\u00f3 del dispositiu per despertar-lo."
}
}
},
diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json
new file mode 100644
index 00000000000000..aad0d1fa47dcf5
--- /dev/null
+++ b/homeassistant/components/shelly/translations/de.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "flow_title": "Shelly: {name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "title": "Shelly"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json
new file mode 100644
index 00000000000000..753e7ee7505a8f
--- /dev/null
+++ b/homeassistant/components/shelly/translations/el.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "unsupported_firmware": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd."
+ },
+ "error": {
+ "auth_not_supported": "\u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Shelly \u03c0\u03bf\u03c5 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd."
+ },
+ "flow_title": "Shelly: {\u03cc\u03bd\u03bf\u03bc\u03b1}",
+ "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."
+ },
+ "user": {
+ "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."
+ }
+ }
+ },
+ "title": "Shelly"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json
index 546007af0d1cdd..2727cfbceeb08e 100644
--- a/homeassistant/components/shelly/translations/en.json
+++ b/homeassistant/components/shelly/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Device is already configured",
+ "unsupported_firmware": "The device is using an unsupported firmware version."
},
"error": {
"auth_not_supported": "Shelly devices requiring authentication are not currently supported.",
@@ -12,7 +13,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Do you want to set up the {model} at {host}?"
+ "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, the battery-powered device must be woken up by pressing the button on the device."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Before set up, the battery-powered device must be woken up by pressing the button on the device."
}
}
},
diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json
index bdc05b734ba8b3..814586abdaea1a 100644
--- a/homeassistant/components/shelly/translations/es.json
+++ b/homeassistant/components/shelly/translations/es.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "El dispositivo ya est\u00e1 configurado"
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "unsupported_firmware": "El dispositivo est\u00e1 usando una versi\u00f3n de firmware no compatible."
},
"error": {
"auth_not_supported": "Los dispositivos Shelly que requieren autenticaci\u00f3n no son compatibles actualmente.",
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo."
}
}
},
diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/shelly/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json
index 595a57b0a00210..8f878924010c01 100644
--- a/homeassistant/components/shelly/translations/it.json
+++ b/homeassistant/components/shelly/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",
+ "unsupported_firmware": "Il dispositivo utilizza una versione del firmware non supportata."
},
"error": {
"auth_not_supported": "I dispositivi Shelly che richiedono l'autenticazione non sono attualmente supportati.",
@@ -12,7 +13,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Vuoi impostare {model} su {host}?"
+ "description": "Vuoi impostare il {model} presso {host}?\n\nPrima di configurare, il dispositivo alimentato a batteria deve essere svegliato premendo il pulsante sul dispositivo."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Prima di configurare, il dispositivo alimentato a batteria deve essere svegliato premendo il pulsante sul dispositivo."
}
}
},
diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json
new file mode 100644
index 00000000000000..5fb84e0ac90486
--- /dev/null
+++ b/homeassistant/components/shelly/translations/ko.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "unsupported_firmware": "\uc774 \uc7a5\uce58\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d"
+ },
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "\uc554\ud638",
+ "username": "\uc0ac\uc6a9\uc790\uba85"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json
index b50f528c3c0058..714b42e9fcce2a 100644
--- a/homeassistant/components/shelly/translations/lb.json
+++ b/homeassistant/components/shelly/translations/lb.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "unsupported_firmware": "Den Apparat benotzt eng net \u00ebnnerst\u00ebtzte Firmware Versioun."
},
"error": {
"auth_not_supported": "Shelly Apparaten d\u00e9i eng Authentifikatioun ben\u00e9idegen ginn aktuell net \u00ebnnerst\u00ebtzt.",
"cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
"unknown": "Onerwaarte Feeler"
},
"flow_title": "Shelly: {name}",
@@ -13,10 +15,17 @@
"confirm_discovery": {
"description": "Soll de {model} um {host} konfigur\u00e9iert ginn?"
},
+ "credentials": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ }
+ },
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Virum ariichten muss dat Batterie bedriwwen Ger\u00e4t aktiv\u00e9iert ginn andeems de Kn\u00e4ppchen um Apparat gedr\u00e9ckt g\u00ebtt."
}
}
},
diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json
new file mode 100644
index 00000000000000..92a172ff081897
--- /dev/null
+++ b/homeassistant/components/shelly/translations/nl.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "auth_not_supported": "Shelly apparaten die verificatie vereisen, worden momenteel niet ondersteund.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "unknown": "Onverwachte fout"
+ },
+ "flow_title": "Shelly: {name}",
+ "step": {
+ "confirm_discovery": {
+ "description": "Wilt u het {model} bij {host} opzetten?"
+ },
+ "credentials": {
+ "data": {
+ "username": "Benutzername"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "title": "Shelly"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json
index 898c05e89aa02a..ac5067d32735a3 100644
--- a/homeassistant/components/shelly/translations/no.json
+++ b/homeassistant/components/shelly/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Enheten er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert",
+ "unsupported_firmware": "Enheten bruker en ikke-st\u00f8ttet firmwareversjon."
},
"error": {
"auth_not_supported": "Shelly-enheter som krever godkjenning st\u00f8ttes for \u00f8yeblikket ikke.",
@@ -12,7 +13,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Vil du konfigurere {model} p\u00e5 {host}?"
+ "description": "Vil du konfigurere {model} p\u00e5 {host} ?\n\n F\u00f8r du setter opp, m\u00e5 den batteridrevne enheten vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Vert"
- }
+ },
+ "description": "F\u00f8r du setter opp, m\u00e5 den batteridrevne enheten vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten."
}
}
},
diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json
index 77a7f045671a71..2266fee5356456 100644
--- a/homeassistant/components/shelly/translations/pl.json
+++ b/homeassistant/components/shelly/translations/pl.json
@@ -1,18 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "unsupported_firmware": "Urz\u0105dzenie u\u017cywa nieobs\u0142ugiwanej wersji firmware."
},
"error": {
"auth_not_supported": "Urz\u0105dzenia Shelly wymagaj\u0105ce uwierzytelnienia nie s\u0105 obecnie obs\u0142ugiwane.",
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?"
+ "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nPrzed konfiguracj\u0105 urz\u0105dzenie zasilane bateryjnie nale\u017cy wybudzi\u0107, naciskaj\u0105c przycisk na urz\u0105dzeniu."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "Nazwa hosta lub adres IP"
- }
+ },
+ "description": "Przed konfiguracj\u0105, urz\u0105dzenie zasilane bateryjnie nale\u017cy wybudzi\u0107, naciskaj\u0105c przycisk na urz\u0105dzeniu."
}
}
},
diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json
index 570e6f8d7c7dee..24478afe0a4d9d 100644
--- a/homeassistant/components/shelly/translations/ru.json
+++ b/homeassistant/components/shelly/translations/ru.json
@@ -1,7 +1,8 @@
{
"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_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.",
+ "unsupported_firmware": "\u0412 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438."
},
"error": {
"auth_not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Shelly, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.",
@@ -12,7 +13,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "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} ({host}) ?"
+ "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} ({host}) ?\n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435."
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442"
- }
+ },
+ "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435."
}
}
},
diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json
index e8fe857c476eea..ebd7df5f9195a0 100644
--- a/homeassistant/components/shelly/translations/zh-Hant.json
+++ b/homeassistant/components/shelly/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "unsupported_firmware": "\u8a2d\u5099\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002"
},
"error": {
"auth_not_supported": "\u76ee\u524d\u4e0d\u652f\u63f4 Shelly \u8a2d\u5099\u6240\u9700\u8a8d\u8b49\u3002",
@@ -12,7 +13,7 @@
"flow_title": "Shelly\uff1a{name}",
"step": {
"confirm_discovery": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u8a2d\u5099\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u8a2d\u5099\u3002"
},
"credentials": {
"data": {
@@ -23,7 +24,8 @@
"user": {
"data": {
"host": "\u4e3b\u6a5f\u7aef"
- }
+ },
+ "description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u8a2d\u5099\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u8a2d\u5099\u3002"
}
}
},
diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json
index 79189e6b047f29..21977c286d08bd 100644
--- a/homeassistant/components/shiftr/manifest.json
+++ b/homeassistant/components/shiftr/manifest.json
@@ -2,6 +2,6 @@
"domain": "shiftr",
"name": "shiftr.io",
"documentation": "https://www.home-assistant.io/integrations/shiftr",
- "requirements": ["paho-mqtt==1.5.0"],
+ "requirements": ["paho-mqtt==1.5.1"],
"codeowners": ["@fabaff"]
}
diff --git a/homeassistant/components/shopping_list/strings.json b/homeassistant/components/shopping_list/strings.json
index 8c17d68388662b..5b8197177a02be 100644
--- a/homeassistant/components/shopping_list/strings.json
+++ b/homeassistant/components/shopping_list/strings.json
@@ -8,7 +8,7 @@
}
},
"abort": {
- "already_configured": "The shopping list is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}
diff --git a/homeassistant/components/shopping_list/translations/ca.json b/homeassistant/components/shopping_list/translations/ca.json
index d384e46641d671..92a206a3e3ac78 100644
--- a/homeassistant/components/shopping_list/translations/ca.json
+++ b/homeassistant/components/shopping_list/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La llista de compres ja est\u00e0 configurada."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/en.json b/homeassistant/components/shopping_list/translations/en.json
index e28b5076dcba8a..2da8b0db8d40f9 100644
--- a/homeassistant/components/shopping_list/translations/en.json
+++ b/homeassistant/components/shopping_list/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "The shopping list is already configured."
+ "already_configured": "Service is already configured"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/it.json b/homeassistant/components/shopping_list/translations/it.json
index c1b10bc84b69b8..bd267168c8f4ed 100644
--- a/homeassistant/components/shopping_list/translations/it.json
+++ b/homeassistant/components/shopping_list/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "La lista della spesa \u00e8 gi\u00e0 configurata."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/no.json b/homeassistant/components/shopping_list/translations/no.json
index 56a92234c70af0..6bad2dd5774d18 100644
--- a/homeassistant/components/shopping_list/translations/no.json
+++ b/homeassistant/components/shopping_list/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Handlelisten er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/ru.json b/homeassistant/components/shopping_list/translations/ru.json
index 84c6e2762f7a87..1dc4a274ab6b6b 100644
--- a/homeassistant/components/shopping_list/translations/ru.json
+++ b/homeassistant/components/shopping_list/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
diff --git a/homeassistant/components/shopping_list/translations/zh-Hant.json b/homeassistant/components/shopping_list/translations/zh-Hant.json
index dbb7d941b2d6f0..9c8520545824eb 100644
--- a/homeassistant/components/shopping_list/translations/zh-Hant.json
+++ b/homeassistant/components/shopping_list/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u8cfc\u7269\u6e05\u55ae\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"step": {
"user": {
diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py
index 6df6e1d0c82f05..1de3cfeb8a0913 100644
--- a/homeassistant/components/sigfox/sensor.py
+++ b/homeassistant/components/sigfox/sensor.py
@@ -8,7 +8,7 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, HTTP_OK
+from homeassistant.const import CONF_NAME, HTTP_OK, HTTP_UNAUTHORIZED
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -67,7 +67,7 @@ def check_credentials(self):
url = urljoin(API_URL, "devicetypes")
response = requests.get(url, auth=self._auth, timeout=10)
if response.status_code != HTTP_OK:
- if response.status_code == 401:
+ if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("Invalid credentials for Sigfox API")
else:
_LOGGER.error(
diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py
index 7e9e789423ebba..e15fab1aaa3139 100644
--- a/homeassistant/components/sighthound/image_processing.py
+++ b/homeassistant/components/sighthound/image_processing.py
@@ -172,7 +172,6 @@ def unit_of_measurement(self):
@property
def device_state_attributes(self):
"""Return the attributes."""
- attr = {}
- if self._last_detection:
- attr["last_person"] = self._last_detection
- return attr
+ if not self._last_detection:
+ return {}
+ return {"last_person": self._last_detection}
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index dd9ab53cb98fcd..613187ef7446ec 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,6 +3,6 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==9.3.0"],
+ "requirements": ["simplisafe-python==9.4.1"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json
index 7f724de9db5f77..44b69bdf6bf9ce 100644
--- a/homeassistant/components/simplisafe/strings.json
+++ b/homeassistant/components/simplisafe/strings.json
@@ -29,7 +29,7 @@
},
"abort": {
"already_configured": "This SimpliSafe account is already in use.",
- "reauth_successful": "SimpliSafe successfully reauthenticated."
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json
index bdfd5d76198725..590f4bbc630e14 100644
--- a/homeassistant/components/simplisafe/translations/ca.json
+++ b/homeassistant/components/simplisafe/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.",
- "reauth_successful": "Reautenticaci\u00f3 amb SimpliSafe exitosa."
+ "reauth_successful": "Re-autenticaci\u00f3 amb SimpliSafe exitosa."
},
"error": {
"identifier_exists": "Aquest compte ja est\u00e0 registrat",
diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json
index 42fc575f650d43..6b71d78673c121 100644
--- a/homeassistant/components/simplisafe/translations/de.json
+++ b/homeassistant/components/simplisafe/translations/de.json
@@ -8,6 +8,11 @@
"invalid_credentials": "Ung\u00fcltige Anmeldeinformationen"
},
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "Passwort"
+ }
+ },
"user": {
"data": {
"code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)",
diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json
index 9f62eb20823c45..54f89eb3ab4d5e 100644
--- a/homeassistant/components/simplisafe/translations/fr.json
+++ b/homeassistant/components/simplisafe/translations/fr.json
@@ -1,17 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9."
+ "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.",
+ "reauth_successful": "SimpliSafe a \u00e9t\u00e9 r\u00e9 authentifi\u00e9 avec succ\u00e8s."
},
"error": {
"identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9",
- "invalid_credentials": "Informations d'identification invalides"
+ "invalid_credentials": "Informations d'identification invalides",
+ "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur",
+ "unknown": "Erreur inattendue"
},
"step": {
+ "mfa": {
+ "description": "V\u00e9rifiez votre messagerie pour un lien de SimpliSafe. Apr\u00e8s avoir v\u00e9rifi\u00e9 le lien, revenez ici pour terminer l'installation de l'int\u00e9gration.",
+ "title": "Authentification multi facteur SimpliSafe"
+ },
"reauth_confirm": {
"data": {
"password": "Mot de passe"
- }
+ },
+ "description": "Votre jeton d'acc\u00e8s a expir\u00e9 ou a \u00e9t\u00e9 r\u00e9voqu\u00e9. Entrez votre mot de passe pour r\u00e9 associer votre compte.",
+ "title": "Relier le compte SimpliSafe"
},
"user": {
"data": {
diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json
index 0562222eea32ba..13f6aa5ffb1d15 100644
--- a/homeassistant/components/simplisafe/translations/pl.json
+++ b/homeassistant/components/simplisafe/translations/pl.json
@@ -1,13 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu."
+ "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu.",
+ "reauth_successful": "SimpliSafe zosta\u0142 ponownie uwierzytelniony."
},
"error": {
"identifier_exists": "Konto jest ju\u017c zarejestrowane.",
- "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce"
+ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
+ "mfa": {
+ "description": "Sprawd\u017a email od SimpliSafe. Po zweryfikowaniu linka, wr\u00f3\u0107 tutaj, aby doko\u0144czy\u0107 instalacj\u0119 integracji.",
+ "title": "Uwierzytelnianie wielosk\u0142adnikowe SimpliSafe"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Has\u0142o"
+ },
+ "description": "Tw\u00f3j token dost\u0119pu wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.",
+ "title": "Po\u0142\u0105cz ponownie konto SimpliSafe"
+ },
"user": {
"data": {
"code": "Kod (u\u017cywany w interfejsie Home Assistanta)",
diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py
index d05448a82c7fd4..fba8b497d51243 100644
--- a/homeassistant/components/simulated/sensor.py
+++ b/homeassistant/components/simulated/sensor.py
@@ -142,7 +142,7 @@ def unit_of_measurement(self):
@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
- attr = {
+ return {
"amplitude": self._amp,
"mean": self._mean,
"period": self._period,
@@ -151,4 +151,3 @@ def device_state_attributes(self):
"seed": self._seed,
"relative_to_epoch": self._relative_to_epoch,
}
- return attr
diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py
index 9606d9bcf12fb3..c1c9d76314c901 100644
--- a/homeassistant/components/skybell/__init__.py
+++ b/homeassistant/components/skybell/__init__.py
@@ -77,11 +77,6 @@ def __init__(self, device):
"""Initialize a sensor for Skybell device."""
self._device = device
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
def update(self):
"""Update automation state."""
self._device.refresh()
diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py
index a5c6681eb2bf62..94f64a4eb43571 100644
--- a/homeassistant/components/skybell/binary_sensor.py
+++ b/homeassistant/components/skybell/binary_sensor.py
@@ -4,7 +4,12 @@
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OCCUPANCY,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
@@ -16,8 +21,8 @@
# Sensor types: Name, device_class, event
SENSOR_TYPES = {
- "button": ["Button", "occupancy", "device:sensor:button"],
- "motion": ["Motion", "motion", "device:sensor:motion"],
+ "button": ["Button", DEVICE_CLASS_OCCUPANCY, "device:sensor:button"],
+ "motion": ["Motion", DEVICE_CLASS_MOTION, "device:sensor:motion"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py
index d43a1fc25ad911..8fb45c36d9f52c 100644
--- a/homeassistant/components/slack/notify.py
+++ b/homeassistant/components/slack/notify.py
@@ -202,22 +202,24 @@ async def _async_send_text_only_message(
self, targets, message, title, blocks, username, icon
):
"""Send a text-only message."""
- if self._icon.lower().startswith(("http://", "https://")):
- icon_type = "url"
- else:
- icon_type = "emoji"
+ message_dict = {
+ "blocks": blocks,
+ "link_names": True,
+ "text": message,
+ "username": username,
+ }
+
+ icon = icon or self._icon
+ if icon:
+ if icon.lower().startswith(("http://", "https://")):
+ icon_type = "url"
+ else:
+ icon_type = "emoji"
+
+ message_dict[f"icon_{icon_type}"] = icon
tasks = {
- target: self._client.chat_postMessage(
- **{
- "blocks": blocks,
- "channel": target,
- "link_names": True,
- "text": message,
- "username": username,
- f"icon_{icon_type}": icon,
- }
- )
+ target: self._client.chat_postMessage(**message_dict, channel=target)
for target in targets
}
diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py
index 39ae3e7c6584b7..cfbd6f576bef97 100644
--- a/homeassistant/components/sleepiq/binary_sensor.py
+++ b/homeassistant/components/sleepiq/binary_sensor.py
@@ -1,5 +1,8 @@
"""Support for SleepIQ sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OCCUPANCY,
+ BinarySensorEntity,
+)
from . import SleepIQSensor
from .const import DOMAIN, IS_IN_BED, SENSOR_TYPES, SIDES
@@ -39,7 +42,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this sensor."""
- return "occupancy"
+ return DEVICE_CLASS_OCCUPANCY
def update(self):
"""Get the latest data from SleepIQ and updates the states."""
diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py
index ecc00f12370088..49d21f2b2c14a5 100644
--- a/homeassistant/components/smappee/binary_sensor.py
+++ b/homeassistant/components/smappee/binary_sensor.py
@@ -1,7 +1,10 @@
"""Support for monitoring a Smappee appliance binary sensor."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_PRESENCE,
+ BinarySensorEntity,
+)
from .const import DOMAIN
@@ -58,7 +61,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return "presence"
+ return DEVICE_CLASS_PRESENCE
@property
def unique_id(
@@ -68,7 +71,7 @@ def unique_id(
return (
f"{self._service_location.device_serial_number}-"
f"{self._service_location.service_location_id}-"
- f"presence"
+ f"{DEVICE_CLASS_PRESENCE}"
)
@property
diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py
index 26a47815f346dd..a31dac6912e282 100644
--- a/homeassistant/components/smappee/config_flow.py
+++ b/homeassistant/components/smappee/config_flow.py
@@ -94,7 +94,7 @@ async def async_step_zeroconf_confirm(self, user_input=None):
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
logon = await self.hass.async_add_executor_job(smappee_api.logon)
if logon is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
return self.async_create_entry(
title=f"{DOMAIN}{serial_number}",
@@ -149,7 +149,7 @@ async def async_step_local(self, user_input=None):
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
logon = await self.hass.async_add_executor_job(smappee_api.logon)
if logon is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
advanced_config = await self.hass.async_add_executor_job(
smappee_api.load_advanced_config
diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json
index 1bec8fda0cc20b..25fa28461173af 100644
--- a/homeassistant/components/smappee/strings.json
+++ b/homeassistant/components/smappee/strings.json
@@ -19,15 +19,15 @@
"title": "Discovered Smappee device"
},
"pick_implementation": {
- "title": "Pick Authentication Method"
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
- "authorize_url_timeout": "Timeout generating authorize url.",
- "connection_error": "Failed to connect to Smappee device.",
- "missing_configuration": "The component is not configured. Please follow the documentation.",
+ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"invalid_mdns": "Unsupported device for the Smappee integration.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json
index df15bdf3ed42f2..ee905f0ca84d50 100644
--- a/homeassistant/components/smappee/translations/ca.json
+++ b/homeassistant/components/smappee/translations/ca.json
@@ -24,7 +24,7 @@
"description": "Introdueix l'amfitri\u00f3 per iniciar la integraci\u00f3 local de Smappee"
},
"pick_implementation": {
- "title": "Selecciona un m\u00e8tode d'autenticaci\u00f3"
+ "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3"
},
"zeroconf_confirm": {
"description": "Vols afegir el dispositiu Smappee amb n\u00famero de s\u00e8rie `{serialnumber}` a Home Assistant?",
diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json
new file mode 100644
index 00000000000000..0e77c8fbd7a44e
--- /dev/null
+++ b/homeassistant/components/smappee/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "flow_title": "Smappee: {name}",
+ "step": {
+ "environment": {
+ "data": {
+ "environment": "Umgebung"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json
index 543c988b356830..5d65081f9f6f01 100644
--- a/homeassistant/components/smappee/translations/es.json
+++ b/homeassistant/components/smappee/translations/es.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"connection_error": "No se pudo conectar al dispositivo Smappee.",
"invalid_mdns": "Dispositivo no compatible para la integraci\u00f3n de Smappee.",
- "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n."
+ "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"flow_title": "Smappee: {name}",
"step": {
diff --git a/homeassistant/components/smappee/translations/et.json b/homeassistant/components/smappee/translations/et.json
new file mode 100644
index 00000000000000..8909689e09869d
--- /dev/null
+++ b/homeassistant/components/smappee/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "connection_error": "Smappee seadmega \u00fchenduse loomine nurjus."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json
index d1dca6c5895897..4bbed6615ca869 100644
--- a/homeassistant/components/smappee/translations/fr.json
+++ b/homeassistant/components/smappee/translations/fr.json
@@ -2,22 +2,33 @@
"config": {
"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.",
- "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
+ "connection_error": "\u00c9chec de la connexion \u00e0 l'appareil Smappee.",
+ "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} )"
},
+ "flow_title": "Smappee: {name}",
"step": {
"environment": {
"data": {
"environment": "Environnement"
- }
+ },
+ "description": "Configurez votre Smappee pour qu'il s'int\u00e8gre \u00e0 Home Assistant."
},
"local": {
"data": {
"host": "H\u00f4te"
- }
+ },
+ "description": "Entrez l'h\u00f4te pour lancer l'int\u00e9gration locale Smappee"
},
"pick_implementation": {
"title": "Choisissez la m\u00e9thode d'authentification"
+ },
+ "zeroconf_confirm": {
+ "description": "Voulez-vous ajouter l'appareil Smappee avec le num\u00e9ro de s\u00e9rie \u00ab {serialnumber} \u00bb \u00e0 Home Assistant?",
+ "title": "Appareil Smappee d\u00e9couvert"
}
}
}
diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json
new file mode 100644
index 00000000000000..5bb10e0f851da1
--- /dev/null
+++ b/homeassistant/components/smappee/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json
index ad85b94abaeddf..3a2cc535c6d069 100644
--- a/homeassistant/components/smappee/translations/it.json
+++ b/homeassistant/components/smappee/translations/it.json
@@ -24,7 +24,7 @@
"description": "Immettere l'host per avviare l'integrazione locale di Smappee"
},
"pick_implementation": {
- "title": "Scegliere il metodo di autenticazione"
+ "title": "Scegli il metodo di autenticazione"
},
"zeroconf_confirm": {
"description": "Vuoi aggiungere il dispositivo Smappee con numero di serie `{serialnumber}` a Home Assistant?",
diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json
index 05557a6046de15..b3e37ee6d01ed0 100644
--- a/homeassistant/components/smappee/translations/ko.json
+++ b/homeassistant/components/smappee/translations/ko.json
@@ -1,10 +1,17 @@
{
"config": {
"abort": {
+ "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"step": {
+ "local": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ }
+ },
"pick_implementation": {
"title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
}
diff --git a/homeassistant/components/smappee/translations/lb.json b/homeassistant/components/smappee/translations/lb.json
index 7f514644918948..90dd0b10486b7a 100644
--- a/homeassistant/components/smappee/translations/lb.json
+++ b/homeassistant/components/smappee/translations/lb.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
"connection_error": "Feeler beim verbannen mam Smappee Apparat.",
"invalid_mdns": "Net \u00ebnnerst\u00ebtzten Apparat fir Smappee Integratioun.",
- "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun."
+ "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"flow_title": "Smappee: {name}",
"step": {
diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json
index 76e96614aa611a..11b8b1bdd30cfc 100644
--- a/homeassistant/components/smappee/translations/no.json
+++ b/homeassistant/components/smappee/translations/no.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.",
"connection_error": "Kunne ikke koble til Smappee-enheten.",
"invalid_mdns": "Ikke-st\u00f8ttet enhet for Smappee-integrasjonen.",
- "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )"
},
"flow_title": "Smappee: {navn}",
"step": {
diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json
index da5a481c22bd33..87cd8ff3e12f01 100644
--- a/homeassistant/components/smappee/translations/pl.json
+++ b/homeassistant/components/smappee/translations/pl.json
@@ -1,12 +1,33 @@
{
"config": {
"abort": {
+ "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_configured_local_device": "Urz\u0105dzenie(-a) lokalne jest ju\u017c skonfigurowane. Usu\u0144 je najpierw przed skonfigurowaniem urz\u0105dzenia w chmurze.",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Smappee.",
+ "invalid_mdns": "Nieobs\u0142ugiwane urz\u0105dzenie dla integracji Smappee.",
"missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
},
+ "flow_title": "Smappee: {name}",
"step": {
+ "environment": {
+ "data": {
+ "environment": "\u015arodowisko"
+ },
+ "description": "Skonfiguruj Smappee, aby zintegrowa\u0107 go z Home Assistant."
+ },
+ "local": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Wprowad\u017a nazw\u0119 hosta, aby zainicjowa\u0107 lokaln\u0105 integracj\u0119 Smappee"
+ },
"pick_implementation": {
"title": "Wybierz metod\u0119 uwierzytelniania"
+ },
+ "zeroconf_confirm": {
+ "description": "Czy chcesz doda\u0107 do Home Assistant urz\u0105dzenie Smappee o numerze seryjnym `{serialnumber}`?",
+ "title": "Wykryto urz\u0105dzenie Smappee"
}
}
}
diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json
index 7636ea5b34b697..5374c535a12133 100644
--- a/homeassistant/components/smappee/translations/zh-Hant.json
+++ b/homeassistant/components/smappee/translations/zh-Hant.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"connection_error": "Smappee \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002",
"invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u8a2d\u5099\u3002",
- "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"flow_title": "Smappee\uff1a{name}",
"step": {
diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json
new file mode 100644
index 00000000000000..936e9817d927ed
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/de.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "title": "Smart Meter Texas"
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/hu.json b/homeassistant/components/smart_meter_texas/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/nl.json b/homeassistant/components/smart_meter_texas/translations/nl.json
new file mode 100644
index 00000000000000..a40ab60cd1e214
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/pl.json b/homeassistant/components/smart_meter_texas/translations/pl.json
new file mode 100644
index 00000000000000..19257e3cf5c626
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Podaj swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o do Smart Meter Texas."
+ }
+ }
+ },
+ "title": "Smart Meter Texas"
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py
index 2d22841660afaa..c826b5d8f4d2b0 100644
--- a/homeassistant/components/smarthab/__init__.py
+++ b/homeassistant/components/smarthab/__init__.py
@@ -34,15 +34,18 @@ async def async_setup(hass, config) -> bool:
"""Set up the SmartHab platform."""
hass.data.setdefault(DOMAIN, {})
- sh_conf = config.get(DOMAIN)
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=sh_conf,
+ 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
diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py
index f0a1df88695636..a277388c140b8e 100644
--- a/homeassistant/components/smarthab/config_flow.py
+++ b/homeassistant/components/smarthab/config_flow.py
@@ -16,6 +16,9 @@
class SmartHabConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""SmartHab config flow."""
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
def _show_setup_form(self, user_input=None, errors=None):
"""Show the setup form to the user."""
@@ -72,6 +75,6 @@ async def async_step_user(self, user_input=None):
return self._show_setup_form(user_input, errors)
- async def async_step_import(self, user_input):
+ async def async_step_import(self, import_info):
"""Handle import from legacy config."""
- return await self.async_step_user(user_input)
+ return await self.async_step_user(import_info)
diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json
index 7279eb6ca79123..3d366edbf73a8d 100644
--- a/homeassistant/components/smarthab/translations/pl.json
+++ b/homeassistant/components/smarthab/translations/pl.json
@@ -2,8 +2,8 @@
"config": {
"error": {
"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_error": "Nieoczekiwany b\u0142\u0105d.",
- "wrong_login": "Niepoprawne uwierzytelnienie."
+ "unknown_error": "Nieoczekiwany b\u0142\u0105d",
+ "wrong_login": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py
index 974cde35faf426..d184a3ca6ce20f 100644
--- a/homeassistant/components/smartthings/__init__.py
+++ b/homeassistant/components/smartthings/__init__.py
@@ -14,6 +14,7 @@
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
HTTP_FORBIDDEN,
+ HTTP_UNAUTHORIZED,
)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -158,7 +159,7 @@ async def retrieve_device_status(device):
hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker
except ClientResponseError as ex:
- if ex.status in (401, HTTP_FORBIDDEN):
+ if ex.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
_LOGGER.exception(
"Unable to setup configuration entry '%s' - please reconfigure the integration",
entry.title,
diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py
index 825cf14995216f..41e915d5c955f4 100644
--- a/homeassistant/components/smartthings/binary_sensor.py
+++ b/homeassistant/components/smartthings/binary_sensor.py
@@ -3,7 +3,16 @@
from pysmartthings import Attribute, Capability
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_MOVING,
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_PRESENCE,
+ DEVICE_CLASS_PROBLEM,
+ DEVICE_CLASS_SOUND,
+ BinarySensorEntity,
+)
from . import SmartThingsEntity
from .const import DATA_BROKERS, DOMAIN
@@ -20,15 +29,15 @@
Capability.water_sensor: Attribute.water,
}
ATTRIB_TO_CLASS = {
- Attribute.acceleration: "moving",
- Attribute.contact: "opening",
- Attribute.filter_status: "problem",
- Attribute.motion: "motion",
- Attribute.presence: "presence",
- Attribute.sound: "sound",
- Attribute.tamper: "problem",
- Attribute.valve: "opening",
- Attribute.water: "moisture",
+ Attribute.acceleration: DEVICE_CLASS_MOVING,
+ Attribute.contact: DEVICE_CLASS_OPENING,
+ Attribute.filter_status: DEVICE_CLASS_PROBLEM,
+ Attribute.motion: DEVICE_CLASS_MOTION,
+ Attribute.presence: DEVICE_CLASS_PRESENCE,
+ Attribute.sound: DEVICE_CLASS_SOUND,
+ Attribute.tamper: DEVICE_CLASS_PROBLEM,
+ Attribute.valve: DEVICE_CLASS_OPENING,
+ Attribute.water: DEVICE_CLASS_MOISTURE,
}
diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json
index 58ea833cb7d7ad..30ef278d1d1153 100644
--- a/homeassistant/components/smartthings/manifest.json
+++ b/homeassistant/components/smartthings/manifest.json
@@ -3,7 +3,7 @@
"name": "SmartThings",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smartthings",
- "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.3"],
+ "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.4"],
"dependencies": ["webhook"],
"after_dependencies": ["cloud"],
"codeowners": ["@andrewsayre"]
diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py
index 7de3b98b1da497..f0240886913efe 100644
--- a/homeassistant/components/smartthings/sensor.py
+++ b/homeassistant/components/smartthings/sensor.py
@@ -5,6 +5,7 @@
from pysmartthings import Attribute, Capability
from homeassistant.const import (
+ AREA_SQUARE_METERS,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
@@ -12,6 +13,7 @@
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
MASS_KILOGRAMS,
PERCENTAGE,
POWER_WATT,
@@ -41,7 +43,12 @@
Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY)
],
Capability.body_mass_index_measurement: [
- Map(Attribute.bmi_measurement, "Body Mass Index", f"{MASS_KILOGRAMS}/m^2", None)
+ Map(
+ Attribute.bmi_measurement,
+ "Body Mass Index",
+ f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}",
+ None,
+ )
],
Capability.body_weight_measurement: [
Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None)
@@ -110,7 +117,7 @@
)
],
Capability.illuminance_measurement: [
- Map(Attribute.illuminance, "Illuminance", "lux", DEVICE_CLASS_ILLUMINANCE)
+ Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE)
],
Capability.infrared_level: [
Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None)
diff --git a/homeassistant/components/smartthings/translations/et.json b/homeassistant/components/smartthings/translations/et.json
new file mode 100644
index 00000000000000..91299004ed30a0
--- /dev/null
+++ b/homeassistant/components/smartthings/translations/et.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "select_location": {
+ "data": {
+ "location_id": "Asukoht"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json
index c355c437689ed7..6051cbbabce9ff 100644
--- a/homeassistant/components/smartthings/translations/fr.json
+++ b/homeassistant/components/smartthings/translations/fr.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "invalid_webhook_url": "Home Assistant n'est pas configur\u00e9 correctement pour recevoir les mises \u00e0 jour de SmartThings. L'URL du webhook n'est pas valide: \n > {webhook_url} \n\n Veuillez mettre \u00e0 jour votre configuration en suivant les [instructions] ({component_url}), red\u00e9marrez Home Assistant et r\u00e9essayez.",
+ "no_available_locations": "Il n'y a pas d'emplacements SmartThings disponibles \u00e0 configurer dans Home Assistant."
+ },
"error": {
"app_setup_error": "Impossible de configurer la SmartApp. Veuillez r\u00e9essayer.",
"token_forbidden": "Le jeton n'a pas les port\u00e9es OAuth requises.",
@@ -15,12 +19,14 @@
"data": {
"access_token": "Jeton d'acc\u00e8s"
},
+ "description": "Veuillez saisir un [jeton d'acc\u00e8s personnel] {token_url} ( {token_url} ) qui a \u00e9t\u00e9 cr\u00e9\u00e9 conform\u00e9ment aux [instructions] ( {component_url} ). Cela sera utilis\u00e9 pour cr\u00e9er l'int\u00e9gration de Home Assistant dans votre compte SmartThings.",
"title": "Entrer un jeton d'acc\u00e8s personnel"
},
"select_location": {
"data": {
"location_id": "Emplacement"
},
+ "description": "Veuillez s\u00e9lectionner l'emplacement SmartThings que vous souhaitez ajouter \u00e0 Home Assistant. Nous ouvrirons alors une nouvelle fen\u00eatre et vous demanderons de vous connecter et d'autoriser l'installation de l'int\u00e9gration de Home Assistant \u00e0 l'emplacement s\u00e9lectionn\u00e9.",
"title": "S\u00e9lectionnez l'emplacement"
},
"user": {
diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py
index f8b9114ae0ee76..965102f07f7c78 100644
--- a/homeassistant/components/smarty/binary_sensor.py
+++ b/homeassistant/components/smarty/binary_sensor.py
@@ -2,7 +2,10 @@
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -83,7 +86,9 @@ class AlarmSensor(SmartyBinarySensor):
def __init__(self, name, smarty):
"""Alarm Sensor Init."""
- super().__init__(name=f"{name} Alarm", device_class="problem", smarty=smarty)
+ super().__init__(
+ name=f"{name} Alarm", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty
+ )
def update(self) -> None:
"""Update state."""
@@ -96,7 +101,9 @@ class WarningSensor(SmartyBinarySensor):
def __init__(self, name, smarty):
"""Warning Sensor Init."""
- super().__init__(name=f"{name} Warning", device_class="problem", smarty=smarty)
+ super().__init__(
+ name=f"{name} Warning", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty
+ )
def update(self) -> None:
"""Update state."""
diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json
index 245260b0fce428..0bba0c2ab48bef 100644
--- a/homeassistant/components/smhi/strings.json
+++ b/homeassistant/components/smhi/strings.json
@@ -4,9 +4,9 @@
"user": {
"title": "Location in Sweden",
"data": {
- "name": "Name",
- "latitude": "Latitude",
- "longitude": "Longitude"
+ "name": "[%key:common::config_flow::data::name%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
diff --git a/homeassistant/components/smhi/translations/et.json b/homeassistant/components/smhi/translations/et.json
new file mode 100644
index 00000000000000..984b43015d77c5
--- /dev/null
+++ b/homeassistant/components/smhi/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "wrong_location": "Asukoht saab olla ainult Rootsis"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Nimi"
+ },
+ "title": "Asukoht Rootsis"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/translations/it.json b/homeassistant/components/smhi/translations/it.json
index b45b60babfe81e..e68f697c678a8b 100644
--- a/homeassistant/components/smhi/translations/it.json
+++ b/homeassistant/components/smhi/translations/it.json
@@ -8,7 +8,7 @@
"user": {
"data": {
"latitude": "Latitudine",
- "longitude": "Longitudine",
+ "longitude": "Logitudine",
"name": "Nome"
},
"title": "Localit\u00e0 in Svezia"
diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py
index 2d36444c03d90e..9bdfbad0f5597f 100644
--- a/homeassistant/components/sms/gateway.py
+++ b/homeassistant/components/sms/gateway.py
@@ -59,9 +59,11 @@ def sms_callback(self, state_machine, callback_type, callback_data):
if inner_entry["Buffer"] is not None:
text = text + inner_entry["Buffer"]
- event_data = dict(
- phone=message["Number"], date=str(message["DateTime"]), message=text
- )
+ event_data = {
+ "phone": message["Number"],
+ "date": str(message["DateTime"]),
+ "message": text,
+ }
_LOGGER.debug("Append event data:%s", event_data)
data.append(event_data)
diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py
index 64d2bf9bd987a8..eaad395eaa6910 100644
--- a/homeassistant/components/sms/sensor.py
+++ b/homeassistant/components/sms/sensor.py
@@ -3,7 +3,7 @@
import gammu # pylint: disable=import-error, no-member
-from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH
+from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, SMS_GATEWAY
@@ -50,7 +50,7 @@ def name(self):
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return "dB"
+ return SIGNAL_STRENGTH_DECIBELS
@property
def device_class(self):
diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json
new file mode 100644
index 00000000000000..273daf6ef0a9e8
--- /dev/null
+++ b/homeassistant/components/sms/translations/de.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "device": "Ger\u00e4t"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/fr.json b/homeassistant/components/sms/translations/fr.json
index 25c08a1e7fe875..b4c479cfd50ec2 100644
--- a/homeassistant/components/sms/translations/fr.json
+++ b/homeassistant/components/sms/translations/fr.json
@@ -12,7 +12,8 @@
"user": {
"data": {
"device": "Appareil"
- }
+ },
+ "title": "Se connecter au modem"
}
}
}
diff --git a/homeassistant/components/sms/translations/hu.json b/homeassistant/components/sms/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/sms/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/pl.json b/homeassistant/components/sms/translations/pl.json
index eec34cc0197c50..bfe331ee89e2c3 100644
--- a/homeassistant/components/sms/translations/pl.json
+++ b/homeassistant/components/sms/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json
index 31eb0491eb4fea..4e65b60280b8d9 100644
--- a/homeassistant/components/snapcast/manifest.json
+++ b/homeassistant/components/snapcast/manifest.json
@@ -2,6 +2,6 @@
"domain": "snapcast",
"name": "Snapcast",
"documentation": "https://www.home-assistant.io/integrations/snapcast",
- "requirements": ["snapcast==2.0.10"],
+ "requirements": ["snapcast==2.1.1"],
"codeowners": []
}
diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py
index 3888b8bf53662c..0cd498b4e3bc9d 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -149,7 +149,7 @@ def __init__(self, platform_name, sensor_key, data_service):
def update(self):
"""Get the latest data from the sensor and update the state."""
self.data_service.update()
- self._state = self.data_service.data[self._json_key]
+ self._state = self.data_service.data.get(self._json_key)
class SolarEdgeDetailsSensor(SolarEdgeSensor):
@@ -192,8 +192,8 @@ def device_state_attributes(self):
def update(self):
"""Get the latest inventory data and update state and attributes."""
self.data_service.update()
- self._state = self.data_service.data[self._json_key]
- self._attributes = self.data_service.attributes[self._json_key]
+ self._state = self.data_service.data.get(self._json_key)
+ self._attributes = self.data_service.attributes.get(self._json_key)
class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor):
@@ -267,7 +267,8 @@ def update(self):
"""Get the latest inventory data and update state and attributes."""
self.data_service.update()
attr = self.data_service.attributes.get(self._json_key)
- self._state = attr["soc"]
+ if attr and "soc" in attr:
+ self._state = attr["soc"]
class SolarEdgeDataService:
diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json
index 1a196315a3031e..068132dea41b9f 100644
--- a/homeassistant/components/solarlog/strings.json
+++ b/homeassistant/components/solarlog/strings.json
@@ -10,11 +10,11 @@
}
},
"error": {
- "already_configured": "Device is already configured",
- "cannot_connect": "Failed to connect, please verify host address"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/solarlog/translations/ca.json b/homeassistant/components/solarlog/translations/ca.json
index c62b69fa976459..6b0883b51fd426 100644
--- a/homeassistant/components/solarlog/translations/ca.json
+++ b/homeassistant/components/solarlog/translations/ca.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "cannot_connect": "No s'ha pogut connectar, verifica l'adre\u00e7a de l'amfitri\u00f3"
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/en.json b/homeassistant/components/solarlog/translations/en.json
index 22dd35574ad6ca..7512da0d2dcd60 100644
--- a/homeassistant/components/solarlog/translations/en.json
+++ b/homeassistant/components/solarlog/translations/en.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "Device is already configured",
- "cannot_connect": "Failed to connect, please verify host address"
+ "cannot_connect": "Failed to connect"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/et.json b/homeassistant/components/solarlog/translations/et.json
new file mode 100644
index 00000000000000..3f33ed633403ee
--- /dev/null
+++ b/homeassistant/components/solarlog/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Kontrolli host'i aadressi"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/translations/it.json b/homeassistant/components/solarlog/translations/it.json
index c227ba7a16cf2e..a4a27c1a0f5f09 100644
--- a/homeassistant/components/solarlog/translations/it.json
+++ b/homeassistant/components/solarlog/translations/it.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "cannot_connect": "Impossibile connettersi, verifica l'indirizzo host"
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/no.json b/homeassistant/components/solarlog/translations/no.json
index 5c0bc0524eb172..437da00617cd29 100644
--- a/homeassistant/components/solarlog/translations/no.json
+++ b/homeassistant/components/solarlog/translations/no.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "Enheten er allerede konfigurert",
- "cannot_connect": "Kunne ikke koble til, vennligst bekreft vertsadresse"
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/pl.json b/homeassistant/components/solarlog/translations/pl.json
index 6769d51c2c27a4..1577982d3d78b7 100644
--- a/homeassistant/components/solarlog/translations/pl.json
+++ b/homeassistant/components/solarlog/translations/pl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta"
},
"step": {
diff --git a/homeassistant/components/solarlog/translations/ru.json b/homeassistant/components/solarlog/translations/ru.json
index cf4adc2d623c6e..ada5ebfc406d30 100644
--- a/homeassistant/components/solarlog/translations/ru.json
+++ b/homeassistant/components/solarlog/translations/ru.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430."
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json
index 85ea05369c5b3a..b8f53a74ff3d5e 100644
--- a/homeassistant/components/solarlog/translations/zh-Hant.json
+++ b/homeassistant/components/solarlog/translations/zh-Hant.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u4f4d\u5740"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
"user": {
diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json
index 5d8590389d820f..bf2d3d72cc53c0 100644
--- a/homeassistant/components/solax/manifest.json
+++ b/homeassistant/components/solax/manifest.json
@@ -2,6 +2,6 @@
"domain": "solax",
"name": "SolaX Power",
"documentation": "https://www.home-assistant.io/integrations/solax",
- "requirements": ["solax==0.2.3"],
+ "requirements": ["solax==0.2.4"],
"codeowners": ["@squishykid"]
}
diff --git a/homeassistant/components/soma/translations/et.json b/homeassistant/components/soma/translations/et.json
new file mode 100644
index 00000000000000..0254817718aa14
--- /dev/null
+++ b/homeassistant/components/soma/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "connection_error": "SOMA Connect seadmega \u00fchenduse loomine nurjus."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index fbc76e7c93811f..7b1430b19c03f9 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -89,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
)
hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation)
+ hass.data[DOMAIN][DEVICES] = []
await update_all_devices(hass)
diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py
index 2d143fbd196327..80fc2192d8e5db 100644
--- a/homeassistant/components/somfy/config_flow.py
+++ b/homeassistant/components/somfy/config_flow.py
@@ -24,6 +24,6 @@ def logger(self) -> logging.Logger:
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
return await super().async_step_user(user_input)
diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json
index d1fa921bb8edf3..85ef981e356e76 100644
--- a/homeassistant/components/somfy/strings.json
+++ b/homeassistant/components/somfy/strings.json
@@ -1,14 +1,18 @@
{
"config": {
"step": {
- "pick_implementation": { "title": "Pick Authentication Method" }
+ "pick_implementation": {
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+ }
},
"abort": {
- "already_setup": "You can only configure one Somfy account.",
- "authorize_url_timeout": "Timeout generating authorize url.",
- "missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
+ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
- "create_entry": { "default": "Successfully authenticated with Somfy." }
+ "create_entry": {
+ "default": "[%key:common::config_flow::create_entry::authenticated%]"
+ }
}
}
diff --git a/homeassistant/components/somfy/translations/ca.json b/homeassistant/components/somfy/translations/ca.json
index 489ab7a7f9f959..55729e859a9627 100644
--- a/homeassistant/components/somfy/translations/ca.json
+++ b/homeassistant/components/somfy/translations/ca.json
@@ -2,12 +2,13 @@
"config": {
"abort": {
"already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Somfy.",
- "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
- "missing_configuration": "El component Somfy no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
- "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})"
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
+ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
+ "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
- "default": "Autenticaci\u00f3 exitosa amb Somfy."
+ "default": "Autenticaci\u00f3 exitosa"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/somfy/translations/el.json b/homeassistant/components/somfy/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/somfy/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/en.json b/homeassistant/components/somfy/translations/en.json
index 2a2bb689860ada..9dede6ab53fd39 100644
--- a/homeassistant/components/somfy/translations/en.json
+++ b/homeassistant/components/somfy/translations/en.json
@@ -2,12 +2,13 @@
"config": {
"abort": {
"already_setup": "You can only configure one Somfy account.",
- "authorize_url_timeout": "Timeout generating authorize url.",
- "missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
- "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
+ "authorize_url_timeout": "Timeout generating authorize URL.",
+ "missing_configuration": "The component is not configured. Please follow the documentation.",
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
- "default": "Successfully authenticated with Somfy."
+ "default": "Successfully authenticated"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json
index bbb1cedad98ff6..6d11afcba4737c 100644
--- a/homeassistant/components/somfy/translations/es.json
+++ b/homeassistant/components/somfy/translations/es.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "Solo puedes configurar una cuenta de Somfy.",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n",
- "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n."
+ "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Autenticado correctamente con Somfy."
diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json
index 5df01fe951be16..3214b3a36deff2 100644
--- a/homeassistant/components/somfy/translations/fr.json
+++ b/homeassistant/components/somfy/translations/fr.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "Vous ne pouvez configurer qu'un seul compte Somfy.",
"authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration d'url autoriser.",
- "missing_configuration": "Le composant Somfy n'est pas configur\u00e9. Veuillez suivre la documentation."
+ "missing_configuration": "Le composant Somfy 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} )",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy."
diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json
index 001739a7a99015..20b645159ccde5 100644
--- a/homeassistant/components/somfy/translations/it.json
+++ b/homeassistant/components/somfy/translations/it.json
@@ -2,16 +2,17 @@
"config": {
"abort": {
"already_setup": "\u00c8 possibile configurare un solo account Somfy.",
- "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione",
- "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione.",
- "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
+ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
- "default": "Autenticato con successo con Somfy."
+ "default": "Autenticazione riuscita"
},
"step": {
"pick_implementation": {
- "title": "Seleziona il metodo di autenticazione"
+ "title": "Scegli il metodo di autenticazione"
}
}
}
diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json
index 9748da483bfeeb..43d4ba146b321a 100644
--- a/homeassistant/components/somfy/translations/ko.json
+++ b/homeassistant/components/somfy/translations/ko.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "\ud558\ub098\uc758 Somfy \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Somfy \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "Somfy \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})",
+ "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
},
"create_entry": {
"default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
diff --git a/homeassistant/components/somfy/translations/lb.json b/homeassistant/components/somfy/translations/lb.json
index f34c07efd06c79..849e2fbadceb38 100644
--- a/homeassistant/components/somfy/translations/lb.json
+++ b/homeassistant/components/somfy/translations/lb.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Somfy Kont konfigur\u00e9ieren.",
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
- "missing_configuration": "D'Somfy Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun."
+ "missing_configuration": "D'Somfy Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Somfy authentifiz\u00e9iert."
diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json
index 6f8e3c3b993686..e5c9c388dd3528 100644
--- a/homeassistant/components/somfy/translations/no.json
+++ b/homeassistant/components/somfy/translations/no.json
@@ -2,11 +2,13 @@
"config": {
"abort": {
"already_setup": "Du kan kun konfigurere \u00e9n Somfy-konto.",
- "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.",
- "missing_configuration": "Somfy-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
+ "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
- "default": "Vellykket godkjenning med Somfy."
+ "default": "Vellykket godkjenning"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json
index 46c1e080480fd5..16205dabc3f1cd 100644
--- a/homeassistant/components/somfy/translations/ru.json
+++ b/homeassistant/components/somfy/translations/ru.json
@@ -3,8 +3,9 @@
"abort": {
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
- "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
- "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435."
+ "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
+ "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."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json
index b5875aaf088adb..4d3d5dc85fba81 100644
--- a/homeassistant/components/somfy/translations/zh-Hant.json
+++ b/homeassistant/components/somfy/translations/zh-Hant.json
@@ -2,11 +2,13 @@
"config": {
"abort": {
"already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Somfy \u5e33\u865f\u3002",
- "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642",
- "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
- "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002"
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py
index b849a490940443..ac3bf0673f180d 100644
--- a/homeassistant/components/somfy_mylink/cover.py
+++ b/homeassistant/components/somfy_mylink/cover.py
@@ -1,7 +1,11 @@
"""Cover Platform for the Somfy MyLink component."""
import logging
-from homeassistant.components.cover import ENTITY_ID_FORMAT, CoverEntity
+from homeassistant.components.cover import (
+ DEVICE_CLASS_WINDOW,
+ ENTITY_ID_FORMAT,
+ CoverEntity,
+)
from homeassistant.util import slugify
from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK
@@ -49,7 +53,7 @@ def __init__(
target_id,
name="SomfyShade",
reverse=False,
- device_class="window",
+ device_class=DEVICE_CLASS_WINDOW,
):
"""Initialize the cover."""
self.somfy_mylink = somfy_mylink
diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py
index 601509aa5755fe..8cb64bb527a892 100644
--- a/homeassistant/components/sonarr/__init__.py
+++ b/homeassistant/components/sonarr/__init__.py
@@ -1,16 +1,19 @@
"""The Sonarr component."""
import asyncio
from datetime import timedelta
+import logging
from typing import Any, Dict
-from sonarr import Sonarr, SonarrError
+from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.components import persistent_notification
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
ATTR_NAME,
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
+ CONF_SOURCE,
CONF_SSL,
CONF_VERIFY_SSL,
)
@@ -36,6 +39,7 @@
PLATFORMS = ["sensor"]
SCAN_INTERVAL = timedelta(seconds=30)
+_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
@@ -69,6 +73,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
try:
await sonarr.update()
+ except SonarrAccessRestricted:
+ _async_start_reauth(hass, entry)
+ return False
except SonarrError as err:
raise ConfigEntryNotReady from err
@@ -106,6 +113,24 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
return unload_ok
+def _async_start_reauth(hass: HomeAssistantType, entry: ConfigEntry):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_REAUTH},
+ data={"config_entry_id": entry.entry_id, **entry.data},
+ )
+ )
+ _LOGGER.error("API Key is no longer valid. Please reauthenticate")
+
+ persistent_notification.async_create(
+ hass,
+ f"Sonarr integration for the Sonarr API hosted at {entry.entry_data[CONF_HOST]} needs to be re-authenticated. Please go to the integrations page to re-configure it.",
+ "Sonarr re-authentication",
+ "sonarr_reauth",
+ )
+
+
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None:
"""Handle options update."""
async_dispatcher_send(
diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py
index ec1a29c660bc4f..753fb829268a79 100644
--- a/homeassistant/components/sonarr/config_flow.py
+++ b/homeassistant/components/sonarr/config_flow.py
@@ -5,6 +5,7 @@
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
import voluptuous as vol
+from homeassistant.components import persistent_notification
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow
from homeassistant.const import (
CONF_API_KEY,
@@ -61,6 +62,12 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
+ def __init__(self):
+ """Initialize the flow."""
+ self._reauth = False
+ self._entry_id = None
+ self._entry_data = {}
+
@staticmethod
@callback
def async_get_options_flow(config_entry):
@@ -73,30 +80,87 @@ async def async_step_import(
"""Handle a flow initiated by configuration file."""
return await self.async_step_user(user_input)
+ async def async_step_reauth(
+ self, data: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Handle configuration by re-auth."""
+ self._reauth = True
+ self._entry_data = dict(data)
+ self._entry_id = self._entry_data.pop("config_entry_id")
+
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(
+ self, user_input: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Confirm reauth dialog."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ description_placeholders={"host": self._entry_data[CONF_HOST]},
+ data_schema=vol.Schema({}),
+ errors={},
+ )
+
+ assert self.hass
+ persistent_notification.async_dismiss(self.hass, "sonarr_reauth")
+
+ return await self.async_step_user()
+
async def async_step_user(
self, user_input: Optional[ConfigType] = None
) -> Dict[str, Any]:
"""Handle a flow initiated by the user."""
- if user_input is None:
- return self._show_setup_form()
+ errors = {}
- if CONF_VERIFY_SSL not in user_input:
- user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
+ if user_input is not None:
+ if self._reauth:
+ user_input = {**self._entry_data, **user_input}
+
+ if CONF_VERIFY_SSL not in user_input:
+ user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
+
+ try:
+ await validate_input(self.hass, user_input)
+ except SonarrAccessRestricted:
+ errors = {"base": "invalid_auth"}
+ except SonarrError:
+ errors = {"base": "cannot_connect"}
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+ else:
+ if self._reauth:
+ return await self._async_reauth_update_entry(
+ self._entry_id, user_input
+ )
+
+ return self.async_create_entry(
+ title=user_input[CONF_HOST], data=user_input
+ )
+
+ data_schema = self._get_user_data_schema()
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(data_schema),
+ errors=errors,
+ )
- try:
- await validate_input(self.hass, user_input)
- except SonarrAccessRestricted:
- return self._show_setup_form({"base": "invalid_auth"})
- except SonarrError:
- return self._show_setup_form({"base": "cannot_connect"})
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- return self.async_abort(reason="unknown")
+ async def _async_reauth_update_entry(
+ self, entry_id: str, data: dict
+ ) -> Dict[str, Any]:
+ """Update existing config entry."""
+ entry = self.hass.config_entries.async_get_entry(entry_id)
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ await self.hass.config_entries.async_reload(entry.entry_id)
- return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
+ return self.async_abort(reason="reauth_successful")
+
+ def _get_user_data_schema(self) -> Dict[str, Any]:
+ """Get the data schema to display user form."""
+ if self._reauth:
+ return {vol.Required(CONF_API_KEY): str}
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
- """Show the setup form to the user."""
data_schema = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_API_KEY): str,
@@ -110,11 +174,7 @@ def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL)
] = bool
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(data_schema),
- errors=errors or {},
- )
+ return data_schema
class SonarrOptionsFlowHandler(OptionsFlow):
diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json
index 3cd6e88913b4b1..65146b90759ac1 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": ["sonarr==0.2.3"],
+ "requirements": ["sonarr==0.3.0"],
"config_flow": true,
"quality_scale": "silver"
}
diff --git a/homeassistant/components/sonarr/strings.json b/homeassistant/components/sonarr/strings.json
index 481a3d381f0535..830b2ccdf7a0b6 100644
--- a/homeassistant/components/sonarr/strings.json
+++ b/homeassistant/components/sonarr/strings.json
@@ -10,9 +10,13 @@
"api_key": "[%key:common::config_flow::data::api_key%]",
"base_path": "Path to API",
"port": "[%key:common::config_flow::data::port%]",
- "ssl": "Sonarr uses a SSL certificate",
- "verify_ssl": "Sonarr uses a proper certificate"
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
+ },
+ "reauth_confirm": {
+ "title": "Re-authenticate with Sonarr",
+ "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}"
}
},
"error": {
@@ -21,6 +25,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json
index ed59caf89df53a..7d82b5042ce12e 100644
--- a/homeassistant/components/sonarr/translations/ca.json
+++ b/homeassistant/components/sonarr/translations/ca.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "El servei ja est\u00e0 configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 exitosa",
"unknown": "Error inesperat"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "La integraci\u00f3 de Sonarr ha de tornar a autenticar-se manualment amb l'API de Sonarr allotjada a: {host}",
+ "title": "Re-autenticaci\u00f3 amb Sonarr"
+ },
"user": {
"data": {
"api_key": "Clau API",
"base_path": "Ruta a l'API",
"host": "Amfitri\u00f3",
"port": "Port",
- "ssl": "Sonarr utilitza un certificat SSL",
- "verify_ssl": "Sonarr utilitza un certificat adequat"
+ "ssl": "Utilitza un certificat SSL",
+ "verify_ssl": "Verifica el certificat SSL"
},
"title": "Connexi\u00f3 amb Sonarr"
}
diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json
new file mode 100644
index 00000000000000..f76b9222a410e8
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/el.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "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"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/en.json b/homeassistant/components/sonarr/translations/en.json
index 9e62ea16d77523..6bd9e34c142f9f 100644
--- a/homeassistant/components/sonarr/translations/en.json
+++ b/homeassistant/components/sonarr/translations/en.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Service is already configured",
+ "reauth_successful": "Successfully re-authenticated",
"unknown": "Unexpected error"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}",
+ "title": "Re-authenticate with Sonarr"
+ },
"user": {
"data": {
"api_key": "API Key",
"base_path": "Path to API",
"host": "Host",
"port": "Port",
- "ssl": "Sonarr uses a SSL certificate",
- "verify_ssl": "Sonarr uses a proper certificate"
+ "ssl": "Uses an SSL certificate",
+ "verify_ssl": "Verify SSL certificate"
},
"title": "Connect to Sonarr"
}
diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json
index 29db7cfbd77f67..343af035865165 100644
--- a/homeassistant/components/sonarr/translations/es.json
+++ b/homeassistant/components/sonarr/translations/es.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "El servicio ya est\u00e1 configurado",
+ "reauth_successful": "Se ha vuelto autenticar con \u00e9xito",
"unknown": "Error inesperado"
},
"error": {
@@ -10,6 +11,10 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "La integraci\u00f3n de Sonarr necesita volver a autenticarse manualmente con la API de Sonarr alojada en: {host}",
+ "title": "Volver a autenticarse con Sonarr"
+ },
"user": {
"data": {
"api_key": "Clave API",
diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json
new file mode 100644
index 00000000000000..946402a1731741
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/et.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "reauth_confirm": {
+ "title": "Autentige uuesti Sonarriga"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json
index 0b35d915f2bd01..91d3b0db419c98 100644
--- a/homeassistant/components/sonarr/translations/fr.json
+++ b/homeassistant/components/sonarr/translations/fr.json
@@ -10,6 +10,9 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "title": "R\u00e9-authentifier avec Sonarr"
+ },
"user": {
"data": {
"api_key": "Cl\u00e9 API",
diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json
new file mode 100644
index 00000000000000..f5301e874eae05
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json
index ae6774db11d294..87e6d1ecd7fb3c 100644
--- a/homeassistant/components/sonarr/translations/it.json
+++ b/homeassistant/components/sonarr/translations/it.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "Ri-autenticato con successo",
"unknown": "Errore imprevisto"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}",
+ "title": "Eseguire nuovamente l'autenticazione con Sonarr"
+ },
"user": {
"data": {
"api_key": "Chiave API",
"base_path": "Percorso dell'API",
"host": "Host",
"port": "Porta",
- "ssl": "Sonarr utilizza un certificato SSL",
- "verify_ssl": "Sonarr utilizza un certificato adeguato"
+ "ssl": "Utilizza un certificato SSL",
+ "verify_ssl": "Verificare il certificato SSL"
},
"title": "Connettiti a Sonarr"
}
diff --git a/homeassistant/components/sonarr/translations/lb.json b/homeassistant/components/sonarr/translations/lb.json
index 23c8116498c434..554bb3eebd264c 100644
--- a/homeassistant/components/sonarr/translations/lb.json
+++ b/homeassistant/components/sonarr/translations/lb.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Service ass scho konfigur\u00e9iert",
+ "reauth_successful": "Erfollegr\u00e4ich re-authentifiz\u00e9iert",
"unknown": "Onerwaarte Feeler"
},
"error": {
@@ -10,6 +11,10 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "Sonarr Integratioun muss manuell mat der Sonarr API um {host} re-authentifiz\u00e9iert ginn.",
+ "title": "Mat Sonarr re-authentifiz\u00e9ieren"
+ },
"user": {
"data": {
"api_key": "API Schl\u00ebssel",
diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json
index 694565b72d6074..3cfe42583608f1 100644
--- a/homeassistant/components/sonarr/translations/no.json
+++ b/homeassistant/components/sonarr/translations/no.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Tjenesten er allerede konfigurert",
+ "reauth_successful": "Godkjent p\u00e5 nytt",
"unknown": "Uventet feil"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "",
"step": {
+ "reauth_confirm": {
+ "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}",
+ "title": "Autentiser p\u00e5 nytt med Sonarr"
+ },
"user": {
"data": {
"api_key": "API N\u00f8kkel",
"base_path": "Bane til API",
"host": "Vert",
"port": "",
- "ssl": "Sonarr bruker et SSL-sertifikat",
- "verify_ssl": "Sonarr bruker et riktig sertifikat"
+ "ssl": "Bruker et SSL-sertifikat",
+ "verify_ssl": "Verifisere SSL-sertifikat"
},
"title": "Koble til Sonarr"
}
diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json
index e2c60427b7e2e7..d3038cc29eb1ae 100644
--- a/homeassistant/components/sonarr/translations/pl.json
+++ b/homeassistant/components/sonarr/translations/pl.json
@@ -2,14 +2,18 @@
"config": {
"abort": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "reauth_successful": "Ponowne uwierzytelnianie powiod\u0142o si\u0119",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {host}"
+ },
"user": {
"data": {
"api_key": "Klucz API",
diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json
index 158a3d59391f23..f8f92340bfd084 100644
--- a/homeassistant/components/sonarr/translations/ru.json
+++ b/homeassistant/components/sonarr/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "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.",
+ "reauth_successful": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr: {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}",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ },
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
"base_path": "\u041f\u0443\u0442\u044c \u043a API",
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "Sonarr \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "verify_ssl": "Sonarr \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\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",
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Sonarr"
}
diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json
index dc03a007099e5d..f876907ec90caf 100644
--- a/homeassistant/components/sonarr/translations/zh-Hant.json
+++ b/homeassistant/components/sonarr/translations/zh-Hant.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u5df2\u6210\u529f\u91cd\u65b0\u8a8d\u8b49",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr\uff1a{name}",
"step": {
+ "reauth_confirm": {
+ "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{host}",
+ "title": "\u91cd\u65b0\u8a8d\u8b49 Sonarr"
+ },
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
"base_path": "API \u8def\u5f91",
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "Sonarr \u4f7f\u7528 SSL \u8a8d\u8b49",
- "verify_ssl": "Sonarr \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"title": "\u9023\u7dda\u81f3 Sonarr"
}
diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json
index a5f52833f4e8ab..5975bb955fa9bc 100644
--- a/homeassistant/components/songpal/translations/fr.json
+++ b/homeassistant/components/songpal/translations/fr.json
@@ -11,6 +11,11 @@
"step": {
"init": {
"description": "Voulez-vous configurer {name} ({host})?"
+ },
+ "user": {
+ "data": {
+ "endpoint": "Terminaison"
+ }
}
}
}
diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json
index cc420f0f83a85d..6b5d29e06b384c 100644
--- a/homeassistant/components/songpal/translations/pl.json
+++ b/homeassistant/components/songpal/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"not_songpal_device": "To nie jest urz\u0105dzenie Songpal."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"flow_title": "Sony Songpal {name} ({host})",
"step": {
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index efad23ee1f2e74..0bb15d2949e150 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -3,11 +3,13 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
- "requirements": ["pysonos==0.0.33"],
+ "requirements": ["pysonos==0.0.34"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
}
],
- "codeowners": []
+ "codeowners": [
+ "@cgtobi"
+ ]
}
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 2b50f2864dca59..307fee923a3435 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -1547,6 +1547,13 @@ def library_payload(media_library):
Used by async_browse_media.
"""
+ if not media_library.browse_by_idstring(
+ "tracks",
+ "",
+ max_items=1,
+ ):
+ raise BrowseError("Local library not found")
+
children = []
for item in media_library.browse():
try:
diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json
index 22414453af9cfd..12812d66692922 100644
--- a/homeassistant/components/sonos/strings.json
+++ b/homeassistant/components/sonos/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Sonos is necessary.",
- "no_devices_found": "No Sonos devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/sonos/translations/ca.json b/homeassistant/components/sonos/translations/ca.json
index c7bbae58a414e0..4f9995c6c118c2 100644
--- a/homeassistant/components/sonos/translations/ca.json
+++ b/homeassistant/components/sonos/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Sonos."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/en.json b/homeassistant/components/sonos/translations/en.json
index 3cd46eae627ea1..38aecd5e965d0d 100644
--- a/homeassistant/components/sonos/translations/en.json
+++ b/homeassistant/components/sonos/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No Sonos devices found on the network.",
- "single_instance_allowed": "Only a single configuration of Sonos is necessary."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/it.json b/homeassistant/components/sonos/translations/it.json
index 70353a2613a5cc..1a646649a1bd6f 100644
--- a/homeassistant/components/sonos/translations/it.json
+++ b/homeassistant/components/sonos/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Non sono presenti dispositivi Sonos in rete.",
- "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Sonos."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/no.json b/homeassistant/components/sonos/translations/no.json
index e5e4792eaebcd4..2da0b5a1b0bfd9 100644
--- a/homeassistant/components/sonos/translations/no.json
+++ b/homeassistant/components/sonos/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Kun en konfigurasjon av Sonos er n\u00f8dvendig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/ru.json b/homeassistant/components/sonos/translations/ru.json
index fa153fda3ce77d..f0b6ca6b6bf74c 100644
--- a/homeassistant/components/sonos/translations/ru.json
+++ b/homeassistant/components/sonos/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json
index 8ffa1577abef08..b47280e3a9e2e1 100644
--- a/homeassistant/components/sonos/translations/zh-Hant.json
+++ b/homeassistant/components/sonos/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py
index 75256b60cfbdcb..1fda59207ec6f1 100644
--- a/homeassistant/components/spc/binary_sensor.py
+++ b/homeassistant/components/spc/binary_sensor.py
@@ -3,7 +3,12 @@
from pyspcwebgw.const import ZoneInput, ZoneType
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_SMOKE,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -14,9 +19,9 @@
def _get_device_class(zone_type):
return {
- ZoneType.ALARM: "motion",
- ZoneType.ENTRY_EXIT: "opening",
- ZoneType.FIRE: "smoke",
+ ZoneType.ALARM: DEVICE_CLASS_MOTION,
+ ZoneType.ENTRY_EXIT: DEVICE_CLASS_OPENING,
+ ZoneType.FIRE: DEVICE_CLASS_SMOKE,
ZoneType.TECHNICAL: "power",
}.get(zone_type)
diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py
index 57557d4558a1ef..32562251ed4cfe 100644
--- a/homeassistant/components/speedtestdotnet/__init__.py
+++ b/homeassistant/components/speedtestdotnet/__init__.py
@@ -143,9 +143,12 @@ def update_servers(self):
self.servers[DEFAULT_SERVER] = {}
for server in sorted(
- server_list.values(), key=lambda server: server[0]["country"]
+ server_list.values(),
+ key=lambda server: server[0]["country"] + server[0]["sponsor"],
):
- self.servers[f"{server[0]['country']} - {server[0]['sponsor']}"] = server[0]
+ self.servers[
+ f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}"
+ ] = server[0]
def update_data(self):
"""Get the latest data from speedtest.net."""
diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py
index 57076c2a90bab9..2bc462afdb0bc8 100644
--- a/homeassistant/components/speedtestdotnet/config_flow.py
+++ b/homeassistant/components/speedtestdotnet/config_flow.py
@@ -36,7 +36,7 @@ def async_get_options_flow(config_entry):
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user")
diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json
index f638c25a5494f1..c4b92b16ff347e 100644
--- a/homeassistant/components/speedtestdotnet/strings.json
+++ b/homeassistant/components/speedtestdotnet/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"wrong_server_id": "Server id is not valid"
}
},
diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json
index 2cd81f8af2e2c8..2f09d5e15e57d3 100644
--- a/homeassistant/components/speedtestdotnet/translations/ca.json
+++ b/homeassistant/components/speedtestdotnet/translations/ca.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "Nom\u00e9s cal una \u00fanica inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
"wrong_server_id": "L'identificador del servidor no \u00e9s v\u00e0lid"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json
index 203640d350bcf6..98b5dd7b1837de 100644
--- a/homeassistant/components/speedtestdotnet/translations/en.json
+++ b/homeassistant/components/speedtestdotnet/translations/en.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible.",
"wrong_server_id": "Server id is not valid"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json
index 9c21c8e29a8a72..f9d16bc5a8c4c3 100644
--- a/homeassistant/components/speedtestdotnet/translations/es.json
+++ b/homeassistant/components/speedtestdotnet/translations/es.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "Solo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n.",
"wrong_server_id": "Id del servidor no v\u00e1lido"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json
new file mode 100644
index 00000000000000..b514abe05b64ba
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/et.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "Vajalik on ainult \u00fcks sidumine.",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.",
+ "wrong_server_id": "Serveri ID ei sobi"
+ },
+ "step": {
+ "user": {
+ "description": "Kas soovid seadistada SpeedTesti?",
+ "title": "Seadista SpeedTest"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "retrive_error": "Viga serverite loendi hankimisel"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "manual": "Keela automaatne v\u00e4rskendamine",
+ "scan_interval": "Uuendamise sagedus (minutites)",
+ "server_name": "Testiserveri valimine"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json
index 68f16cea44fd52..7b92a679f25c18 100644
--- a/homeassistant/components/speedtestdotnet/translations/fr.json
+++ b/homeassistant/components/speedtestdotnet/translations/fr.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.",
"wrong_server_id": "L'ID du serveur n'est pas valide"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json
index 3ac6b03183c520..3c915c15265da1 100644
--- a/homeassistant/components/speedtestdotnet/translations/it.json
+++ b/homeassistant/components/speedtestdotnet/translations/it.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "\u00c8 necessaria solo una singola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
"wrong_server_id": "L'ID del server non \u00e8 valido"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/lb.json b/homeassistant/components/speedtestdotnet/translations/lb.json
index b9587e71087c24..7ceedc228e3bd9 100644
--- a/homeassistant/components/speedtestdotnet/translations/lb.json
+++ b/homeassistant/components/speedtestdotnet/translations/lb.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
"wrong_server_id": "Server ID ass ong\u00eblteg"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json
index da2945b2985999..b346f1dae6df8e 100644
--- a/homeassistant/components/speedtestdotnet/translations/no.json
+++ b/homeassistant/components/speedtestdotnet/translations/no.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
"wrong_server_id": "Server-ID er ikke gyldig"
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json
new file mode 100644
index 00000000000000..014c93446f14d4
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/pl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "Wymagana jest tylko jedna instancja.",
+ "wrong_server_id": "Identyfikator serwera jest nieprawid\u0142owy"
+ },
+ "step": {
+ "user": {
+ "description": "Czy na pewno chcesz skonfigurowa\u0107 SpeedTest?",
+ "title": "Konfiguracja SpeedTest"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "retrive_error": "B\u0142\u0105d podczas pobierania listy serwer\u00f3w"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "manual": "Wy\u0142\u0105cz automatyczne aktualizacje",
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)",
+ "server_name": "Wybierz serwer"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json
index fe5cbc1d89073c..e17f1d4d05dc67 100644
--- a/homeassistant/components/speedtestdotnet/translations/ru.json
+++ b/homeassistant/components/speedtestdotnet/translations/ru.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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.",
"wrong_server_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d."
},
"step": {
diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json
index 1661030b2f971f..5a5e072b99d4a3 100644
--- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json
+++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002",
"wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548"
},
"step": {
diff --git a/homeassistant/components/spider/translations/de.json b/homeassistant/components/spider/translations/de.json
new file mode 100644
index 00000000000000..6f39806287630f
--- /dev/null
+++ b/homeassistant/components/spider/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json
index 959a28add769c2..8658343db6ac0c 100644
--- a/homeassistant/components/spider/translations/fr.json
+++ b/homeassistant/components/spider/translations/fr.json
@@ -12,7 +12,8 @@
"data": {
"password": "Mot de passe",
"username": "Nom d'utilisateur"
- }
+ },
+ "title": "Connectez-vous avec le compte mijn.ithodaalderop.nl"
}
}
}
diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json
new file mode 100644
index 00000000000000..1f08b96ee10939
--- /dev/null
+++ b/homeassistant/components/spider/translations/ko.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/spider/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/pl.json b/homeassistant/components/spider/translations/pl.json
new file mode 100644
index 00000000000000..f288bcbd78bb61
--- /dev/null
+++ b/homeassistant/components/spider/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "title": "Zaloguj si\u0119 na konto mijn.ithodaalderop.nl"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py
index bbff510db1487a..a3ec307d67b15e 100644
--- a/homeassistant/components/splunk/__init__.py
+++ b/homeassistant/components/splunk/__init__.py
@@ -1,9 +1,11 @@
-"""Support to send data to an Splunk instance."""
+"""Support to send data to a Splunk instance."""
+import asyncio
import json
import logging
+import time
-from aiohttp.hdrs import AUTHORIZATION
-import requests
+from aiohttp import ClientConnectionError, ClientResponseError
+from hass_splunk import SplunkPayloadError, hass_splunk
import voluptuous as vol
from homeassistant.const import (
@@ -16,14 +18,15 @@
EVENT_STATE_CHANGED,
)
from homeassistant.helpers import state as state_helper
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
-CONF_FILTER = "filter"
DOMAIN = "splunk"
+CONF_FILTER = "filter"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 8088
@@ -48,23 +51,7 @@
)
-def post_request(event_collector, body, headers, verify_ssl):
- """Post request to Splunk."""
- try:
- payload = {"host": event_collector, "event": body}
- requests.post(
- event_collector,
- data=json.dumps(payload, cls=JSONEncoder),
- headers=headers,
- timeout=10,
- verify=verify_ssl,
- )
-
- except requests.exceptions.RequestException as error:
- _LOGGER.exception("Error saving event to Splunk: %s", error)
-
-
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the Splunk component."""
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
@@ -75,18 +62,33 @@ def setup(hass, config):
name = conf.get(CONF_NAME)
entity_filter = conf[CONF_FILTER]
- if use_ssl:
- uri_scheme = "https://"
- else:
- uri_scheme = "http://"
-
- event_collector = f"{uri_scheme}{host}:{port}/services/collector/event"
- headers = {AUTHORIZATION: f"Splunk {token}"}
-
- def splunk_event_listener(event):
+ event_collector = hass_splunk(
+ session=async_get_clientsession(hass),
+ host=host,
+ port=port,
+ token=token,
+ use_ssl=use_ssl,
+ verify_ssl=verify_ssl,
+ )
+
+ if not await event_collector.check(connectivity=False, token=True, busy=False):
+ return False
+
+ payload = {
+ "time": time.time(),
+ "host": name,
+ "event": {
+ "domain": DOMAIN,
+ "meta": "Splunk integration has started",
+ },
+ }
+
+ await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=False)
+
+ async def splunk_event_listener(event):
"""Listen for new messages on the bus and sends them to Splunk."""
- state = event.data.get("new_state")
+ state = event.data.get("new_state")
if state is None or not entity_filter(state.entity_id):
return
@@ -95,19 +97,31 @@ def splunk_event_listener(event):
except ValueError:
_state = state.state
- json_body = [
- {
+ payload = {
+ "time": event.time_fired.timestamp(),
+ "host": name,
+ "event": {
"domain": state.domain,
"entity_id": state.object_id,
"attributes": dict(state.attributes),
- "time": str(event.time_fired),
"value": _state,
- "host": name,
- }
- ]
-
- post_request(event_collector, json_body, headers, verify_ssl)
+ },
+ }
- hass.bus.listen(EVENT_STATE_CHANGED, splunk_event_listener)
+ try:
+ await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=True)
+ except SplunkPayloadError as err:
+ if err.status == 401:
+ _LOGGER.error(err)
+ else:
+ _LOGGER.warning(err)
+ except ClientConnectionError as err:
+ _LOGGER.warning(err)
+ except asyncio.TimeoutError:
+ _LOGGER.warning("Connection to %s:%s timed out", host, port)
+ except ClientResponseError as err:
+ _LOGGER.error(err.message)
+
+ hass.bus.async_listen(EVENT_STATE_CHANGED, splunk_event_listener)
return True
diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json
index 337458b4c3f945..d51d6c712de797 100644
--- a/homeassistant/components/splunk/manifest.json
+++ b/homeassistant/components/splunk/manifest.json
@@ -2,5 +2,10 @@
"domain": "splunk",
"name": "Splunk",
"documentation": "https://www.home-assistant.io/integrations/splunk",
- "codeowners": []
-}
+ "requirements": [
+ "hass_splunk==0.1.1"
+ ],
+ "codeowners": [
+ "@Bre77"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json
index d17cd43c47f1af..db2f35ded91f0e 100644
--- a/homeassistant/components/spotify/manifest.json
+++ b/homeassistant/components/spotify/manifest.json
@@ -2,7 +2,7 @@
"domain": "spotify",
"name": "Spotify",
"documentation": "https://www.home-assistant.io/integrations/spotify",
- "requirements": ["spotipy==2.14.0"],
+ "requirements": ["spotipy==2.16.0"],
"zeroconf": ["_spotify-connect._tcp.local."],
"dependencies": ["http"],
"codeowners": ["@frenck"],
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index 7beea59a7bd219..0782cb2f3908ee 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -105,24 +105,57 @@
}
CONTENT_TYPE_MEDIA_CLASS = {
- "current_user_playlists": MEDIA_CLASS_DIRECTORY,
- "current_user_followed_artists": MEDIA_CLASS_DIRECTORY,
- "current_user_saved_albums": MEDIA_CLASS_DIRECTORY,
- "current_user_saved_tracks": MEDIA_CLASS_DIRECTORY,
- "current_user_saved_shows": MEDIA_CLASS_DIRECTORY,
- "current_user_recently_played": MEDIA_CLASS_DIRECTORY,
- "current_user_top_artists": MEDIA_CLASS_DIRECTORY,
- "current_user_top_tracks": MEDIA_CLASS_DIRECTORY,
- "featured_playlists": MEDIA_CLASS_DIRECTORY,
- "categories": MEDIA_CLASS_DIRECTORY,
- "category_playlists": MEDIA_CLASS_DIRECTORY,
- "new_releases": MEDIA_CLASS_DIRECTORY,
- MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
- MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
- MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
- MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
- MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST,
- MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
+ "current_user_playlists": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_PLAYLIST,
+ },
+ "current_user_followed_artists": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_ARTIST,
+ },
+ "current_user_saved_albums": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_ALBUM,
+ },
+ "current_user_saved_tracks": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_TRACK,
+ },
+ "current_user_saved_shows": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_PODCAST,
+ },
+ "current_user_recently_played": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_TRACK,
+ },
+ "current_user_top_artists": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_ARTIST,
+ },
+ "current_user_top_tracks": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_TRACK,
+ },
+ "featured_playlists": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_PLAYLIST,
+ },
+ "categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE},
+ "category_playlists": {
+ "parent": MEDIA_CLASS_DIRECTORY,
+ "children": MEDIA_CLASS_PLAYLIST,
+ },
+ "new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM},
+ MEDIA_TYPE_PLAYLIST: {
+ "parent": MEDIA_CLASS_PLAYLIST,
+ "children": MEDIA_CLASS_TRACK,
+ },
+ MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK},
+ MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM},
+ MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None},
+ MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE},
+ MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None},
}
@@ -543,7 +576,8 @@ def build_item_response(spotify, user, payload):
if media_content_type == "categories":
media_item = BrowseMedia(
title=LIBRARY_MAP.get(media_content_id),
- media_class=media_class,
+ media_class=media_class["parent"],
+ children_media_class=media_class["children"],
media_content_id=media_content_id,
media_content_type=media_content_type,
can_play=False,
@@ -560,6 +594,7 @@ def build_item_response(spotify, user, payload):
BrowseMedia(
title=item.get("name"),
media_class=MEDIA_CLASS_PLAYLIST,
+ children_media_class=MEDIA_CLASS_TRACK,
media_content_id=item_id,
media_content_type="category_playlists",
thumbnail=fetch_image_url(item, key="icons"),
@@ -567,7 +602,6 @@ def build_item_response(spotify, user, payload):
can_expand=True,
)
)
- media_item.children_media_class = MEDIA_CLASS_GENRE
return media_item
if title is None:
@@ -578,7 +612,8 @@ def build_item_response(spotify, user, payload):
params = {
"title": title,
- "media_class": media_class,
+ "media_class": media_class["parent"],
+ "children_media_class": media_class["children"],
"media_content_id": media_content_id,
"media_content_type": media_content_type,
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
@@ -625,7 +660,8 @@ def item_payload(item):
payload = {
"title": item.get("name"),
- "media_class": media_class,
+ "media_class": media_class["parent"],
+ "children_media_class": media_class["children"],
"media_content_id": media_id,
"media_content_type": media_type,
"can_play": media_type in PLAYABLE_MEDIA_TYPES,
diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json
index 8e3fa6fc6790d0..76cad9bdf804a0 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -1,14 +1,15 @@
{
"config": {
"step": {
- "pick_implementation": { "title": "Pick Authentication Method" },
+ "pick_implementation": {
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+ },
"reauth_confirm": {
"title": "Re-authenticate with Spotify",
"description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}"
}
},
"abort": {
- "already_setup": "You can only configure one Spotify account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json
new file mode 100644
index 00000000000000..d2580d58c0ba9a
--- /dev/null
+++ b/homeassistant/components/spotify/translations/el.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "reauth_account_mismatch": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Spotify \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bf\u03c0\u03bf\u03af\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2, \u03b4\u03b5\u03bd \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd."
+ },
+ "step": {
+ "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"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json
index 95c29f7a33693c..025777ad3f6667 100644
--- a/homeassistant/components/spotify/translations/es.json
+++ b/homeassistant/components/spotify/translations/es.json
@@ -4,6 +4,7 @@
"already_setup": "S\u00f3lo puedes configurar una cuenta de Spotify.",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})",
"reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json
new file mode 100644
index 00000000000000..aaeab0a4039ff5
--- /dev/null
+++ b/homeassistant/components/spotify/translations/et.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Saate konfigureerida ainult \u00fche Spotify konto.",
+ "authorize_url_timeout": "Kinnituse URLi ajal\u00f5pp",
+ "missing_configuration": "Spotify sidumine pole h\u00e4\u00e4lestatud. Palun j\u00e4rgige dokumentatsiooni.",
+ "no_url_available": "URL ploe saadaval. Rohkem teavet [check the help section]({docs_url})",
+ "reauth_account_mismatch": "Spotify konto mida autenditi ei vasta kontole mis vajas uuesti autentimist."
+ },
+ "create_entry": {
+ "default": "Edukalt Spotifyga autenditud."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Valige autentimismeetod"
+ },
+ "reauth_confirm": {
+ "description": "Spotify konto: {account} sidumine tuleb uuesti autentida",
+ "title": "Autendi Spotify uuesti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json
index 629aa5c681fce4..251c85920aa3f4 100644
--- a/homeassistant/components/spotify/translations/fr.json
+++ b/homeassistant/components/spotify/translations/fr.json
@@ -4,6 +4,7 @@
"already_setup": "Vous ne pouvez configurer qu'un seul compte Spotify.",
"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} )",
"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/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json
index deb55479c1e561..37dccd8c1a6af4 100644
--- a/homeassistant/components/spotify/translations/ko.json
+++ b/homeassistant/components/spotify/translations/ko.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "\ud558\ub098\uc758 Spotify \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})",
+ "reauth_account_mismatch": "\uc778\uc99d\ub41c Spotify \uacc4\uc815\uc740 \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud55c \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
"default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
@@ -11,6 +13,10 @@
"step": {
"pick_implementation": {
"title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "reauth_confirm": {
+ "description": "Spotify \ud1b5\ud569\uc740 \uacc4\uc815 {account} \ub300\ud574 Spotify\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.",
+ "title": "Spotify\ub85c \uc7ac \uc778\uc99d"
}
}
}
diff --git a/homeassistant/components/spotify/translations/lb.json b/homeassistant/components/spotify/translations/lb.json
index fedaff526a62c1..59aaf936dc15ca 100644
--- a/homeassistant/components/spotify/translations/lb.json
+++ b/homeassistant/components/spotify/translations/lb.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Spotify Kont konfigur\u00e9ieren.",
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
- "missing_configuration": "Spotifiy Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun."
+ "missing_configuration": "Spotifiy Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Spotify authentifiz\u00e9iert."
diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json
index 82b25512001716..b300066ca95c64 100644
--- a/homeassistant/components/spotify/translations/nl.json
+++ b/homeassistant/components/spotify/translations/nl.json
@@ -3,7 +3,9 @@
"abort": {
"already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.",
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
- "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen."
+ "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ({docs_url})",
+ "reauth_account_mismatch": "Het Spotify account waarmee er is geverifieerd, komt niet overeen met het account dat opnieuw moet worden geverifieerd."
},
"create_entry": {
"default": "Succesvol geauthenticeerd met Spotify."
@@ -11,6 +13,10 @@
"step": {
"pick_implementation": {
"title": "Kies Authenticatiemethode"
+ },
+ "reauth_confirm": {
+ "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}",
+ "title": "Verifieer opnieuw met Spotify"
}
}
}
diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json
index c2e151e5eb7848..6ab60ddbc0c5e8 100644
--- a/homeassistant/components/spotify/translations/no.json
+++ b/homeassistant/components/spotify/translations/no.json
@@ -4,6 +4,7 @@
"already_setup": "Du kan bare konfigurere en Spotify-konto.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.",
"missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )",
"reauth_account_mismatch": "Spotify-kontoen som er autentisert med, samsvarer ikke med den kontoen som trengs re-autentisering."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json
index 0ed5a24ce16c81..06ae75cc0bb60e 100644
--- a/homeassistant/components/spotify/translations/pl.json
+++ b/homeassistant/components/spotify/translations/pl.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Spotify.",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
- "missing_configuration": "Integracja ze Spotify nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105."
+ "missing_configuration": "Integracja ze Spotify nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105.",
+ "reauth_account_mismatch": "Uwierzytelnione konto Spotify, nie pasuje do konta, kt\u00f3re wymaga ponownego uwierzytelnienia."
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono z Spotify"
@@ -13,6 +14,7 @@
"title": "Wybierz metod\u0119 uwierzytelniania"
},
"reauth_confirm": {
+ "description": "Integracja Spotify wymaga ponownego uwierzytelnienia w Spotify dla konta: {account}",
"title": "Ponownie uwierzytelnij ze Spotify"
}
}
diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json
index a4fca36205f120..d7e52012c363d0 100644
--- a/homeassistant/components/spotify/translations/zh-Hant.json
+++ b/homeassistant/components/spotify/translations/zh-Hant.json
@@ -4,6 +4,7 @@
"already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Spotify \u5e33\u865f\u3002",
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"missing_configuration": "Spotify \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
"reauth_account_mismatch": "Spotify \u6240\u8a8d\u8b49\u5e33\u865f\u8207\u5e33\u865f\u4e0d\u7b26\u5408\uff0c\u9700\u91cd\u65b0\u9032\u884c\u8a8d\u8b49\u3002"
},
"create_entry": {
diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py
index f19941ed0433ff..27656c260d38fa 100644
--- a/homeassistant/components/sql/sensor.py
+++ b/homeassistant/components/sql/sensor.py
@@ -122,6 +122,7 @@ def device_state_attributes(self):
def update(self):
"""Retrieve sensor data from the query."""
+ data = None
try:
sess = self.sessionmaker()
result = sess.execute(self._query)
@@ -147,7 +148,7 @@ def update(self):
finally:
sess.close()
- if self._template is not None:
+ if data is not None and self._template is not None:
self._state = self._template.async_render_with_possible_json_value(
data, None
)
diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json
index 4d024c59d26478..8671822cdf8fbd 100644
--- a/homeassistant/components/squeezebox/translations/de.json
+++ b/homeassistant/components/squeezebox/translations/de.json
@@ -4,9 +4,16 @@
"edit": {
"data": {
"password": "Passwort",
+ "port": "Port",
"username": "Benutzername"
}
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
}
}
- }
+ },
+ "title": "Logitech Squeezebox"
}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json
index 9e7445a4a3200c..6119bc34f8d247 100644
--- a/homeassistant/components/squeezebox/translations/fr.json
+++ b/homeassistant/components/squeezebox/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",
+ "no_server_found": "Aucun serveur LMS trouv\u00e9."
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
@@ -17,7 +18,8 @@
"password": "Mot de passe",
"port": "Port",
"username": "Username"
- }
+ },
+ "title": "Modifier les informations de connexion"
},
"user": {
"data": {
diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json
new file mode 100644
index 00000000000000..bb140f4ca899d6
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "edit": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/pl.json b/homeassistant/components/squeezebox/translations/pl.json
index a43397119189b7..4bf27bbb3ec27f 100644
--- a/homeassistant/components/squeezebox/translations/pl.json
+++ b/homeassistant/components/squeezebox/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"edit": {
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
index 555d68cd5d4518..af2ae21dac3641 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -17,16 +17,22 @@
# Attributes for accessing info from SSDP response
ATTR_SSDP_LOCATION = "ssdp_location"
ATTR_SSDP_ST = "ssdp_st"
+ATTR_SSDP_USN = "ssdp_usn"
+ATTR_SSDP_EXT = "ssdp_ext"
+ATTR_SSDP_SERVER = "ssdp_server"
# Attributes for accessing info from retrieved UPnP device description
ATTR_UPNP_DEVICE_TYPE = "deviceType"
ATTR_UPNP_FRIENDLY_NAME = "friendlyName"
ATTR_UPNP_MANUFACTURER = "manufacturer"
ATTR_UPNP_MANUFACTURER_URL = "manufacturerURL"
+ATTR_UPNP_MODEL_DESCRIPTION = "modelDescription"
ATTR_UPNP_MODEL_NAME = "modelName"
ATTR_UPNP_MODEL_NUMBER = "modelNumber"
-ATTR_UPNP_PRESENTATION_URL = "presentationURL"
+ATTR_UPNP_MODEL_URL = "modelURL"
ATTR_UPNP_SERIAL = "serialNumber"
ATTR_UPNP_UDN = "UDN"
+ATTR_UPNP_UPC = "UPC"
+ATTR_UPNP_PRESENTATION_URL = "presentationURL"
_LOGGER = logging.getLogger(__name__)
@@ -107,6 +113,9 @@ async def _process_entry(self, entry):
"""Process a single entry."""
info = {"st": entry.st}
+ for key in "usn", "ext", "server":
+ if key in entry.values:
+ info[key] = entry.values[key]
if entry.location:
@@ -165,5 +174,12 @@ def info_from_entry(entry, device_info):
}
if device_info:
info.update(device_info)
+ info.pop("st", None)
+ if "usn" in info:
+ info[ATTR_SSDP_USN] = info.pop("usn")
+ if "ext" in info:
+ info[ATTR_SSDP_EXT] = info.pop("ext")
+ if "server" in info:
+ info[ATTR_SSDP_SERVER] = info.pop("server")
return info
diff --git a/homeassistant/components/starline/translations/et.json b/homeassistant/components/starline/translations/et.json
new file mode 100644
index 00000000000000..7e3c4103ccf038
--- /dev/null
+++ b/homeassistant/components/starline/translations/et.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "step": {
+ "auth_mfa": {
+ "title": "Kaheastmeline autoriseerimine"
+ },
+ "auth_user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py
index 91c4018d899a2d..b986cddaf68e9d 100644
--- a/homeassistant/components/stream/const.py
+++ b/homeassistant/components/stream/const.py
@@ -19,3 +19,4 @@
MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
+MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable
diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py
index 5e4e85ceea6ef4..20931abf11e1d0 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -84,7 +84,7 @@ def target_duration(self) -> int:
"""Return the max duration of any given segment in seconds."""
segment_length = len(self._segments)
if not segment_length:
- return 0
+ return 1
durations = [s.duration for s in self._segments]
return round(max(durations)) or 1
diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py
index 00603807215a60..dc929e531c1c66 100644
--- a/homeassistant/components/stream/fmp4utils.py
+++ b/homeassistant/components/stream/fmp4utils.py
@@ -36,3 +36,114 @@ def get_m4s(segment: io.BytesIO, sequence: int) -> bytes:
mfra_location = next(find_box(segment, b"mfra"))
segment.seek(moof_location)
return segment.read(mfra_location - moof_location)
+
+
+def get_codec_string(segment: io.BytesIO) -> str:
+ """Get RFC 6381 codec string."""
+ codecs = []
+
+ # Find moov
+ moov_location = next(find_box(segment, b"moov"))
+
+ # Find tracks
+ for trak_location in find_box(segment, b"trak", moov_location):
+ # Drill down to media info
+ mdia_location = next(find_box(segment, b"mdia", trak_location))
+ minf_location = next(find_box(segment, b"minf", mdia_location))
+ stbl_location = next(find_box(segment, b"stbl", minf_location))
+ stsd_location = next(find_box(segment, b"stsd", stbl_location))
+
+ # Get stsd box
+ segment.seek(stsd_location)
+ stsd_length = int.from_bytes(segment.read(4), byteorder="big")
+ segment.seek(stsd_location)
+ stsd_box = segment.read(stsd_length)
+
+ # Base Codec
+ codec = stsd_box[20:24].decode("utf-8")
+
+ # Handle H264
+ if (
+ codec in ("avc1", "avc2", "avc3", "avc4")
+ and stsd_length > 110
+ and stsd_box[106:110] == b"avcC"
+ ):
+ profile = stsd_box[111:112].hex()
+ compatibility = stsd_box[112:113].hex()
+ level = stsd_box[113:114].hex()
+ codec += "." + profile + compatibility + level
+
+ # Handle H265
+ elif (
+ codec in ("hev1", "hvc1")
+ and stsd_length > 110
+ and stsd_box[106:110] == b"hvcC"
+ ):
+ tmp_byte = int.from_bytes(stsd_box[111:112], byteorder="big")
+
+ # Profile Space
+ codec += "."
+ profile_space_map = {0: "", 1: "A", 2: "B", 3: "C"}
+ profile_space = tmp_byte >> 6
+ codec += profile_space_map[profile_space]
+ general_profile_idc = tmp_byte & 31
+ codec += str(general_profile_idc)
+
+ # Compatibility
+ codec += "."
+ general_profile_compatibility = int.from_bytes(
+ stsd_box[112:116], byteorder="big"
+ )
+ reverse = 0
+ for i in range(0, 32):
+ reverse |= general_profile_compatibility & 1
+ if i == 31:
+ break
+ reverse <<= 1
+ general_profile_compatibility >>= 1
+ codec += hex(reverse)[2:]
+
+ # Tier Flag
+ if (tmp_byte & 32) >> 5 == 0:
+ codec += ".L"
+ else:
+ codec += ".H"
+ codec += str(int.from_bytes(stsd_box[122:123], byteorder="big"))
+
+ # Constraint String
+ has_byte = False
+ constraint_string = ""
+ for i in range(121, 115, -1):
+ gci = int.from_bytes(stsd_box[i : i + 1], byteorder="big")
+ if gci or has_byte:
+ constraint_string = "." + hex(gci)[2:] + constraint_string
+ has_byte = True
+ codec += constraint_string
+
+ # Handle Audio
+ elif codec == "mp4a":
+ oti = None
+ dsi = None
+
+ # Parse ES Descriptors
+ oti_loc = stsd_box.find(b"\x04\x80\x80\x80")
+ if oti_loc > 0:
+ oti = stsd_box[oti_loc + 5 : oti_loc + 6].hex()
+ codec += f".{oti}"
+
+ dsi_loc = stsd_box.find(b"\x05\x80\x80\x80")
+ if dsi_loc > 0:
+ dsi_length = int.from_bytes(
+ stsd_box[dsi_loc + 4 : dsi_loc + 5], byteorder="big"
+ )
+ dsi_data = stsd_box[dsi_loc + 5 : dsi_loc + 5 + dsi_length]
+ dsi0 = int.from_bytes(dsi_data[0:1], byteorder="big")
+ dsi = (dsi0 & 248) >> 3
+ if dsi == 31 and len(dsi_data) >= 2:
+ dsi1 = int.from_bytes(dsi_data[1:2], byteorder="big")
+ dsi = 32 + ((dsi0 & 7) << 3) + ((dsi1 & 224) >> 5)
+ codec += f".{dsi}"
+
+ codecs.append(codec)
+
+ return ",".join(codecs)
diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py
index 1e97ac222ec413..09729f79ada099 100644
--- a/homeassistant/components/stream/hls.py
+++ b/homeassistant/components/stream/hls.py
@@ -1,4 +1,5 @@
"""Provide functionality to stream HLS."""
+import io
from typing import Callable
from aiohttp import web
@@ -7,7 +8,7 @@
from .const import FORMAT_CONTENT_TYPE
from .core import PROVIDERS, StreamOutput, StreamView
-from .fmp4utils import get_init, get_m4s
+from .fmp4utils import get_codec_string, get_init, get_m4s
@callback
@@ -16,7 +17,43 @@ def async_setup_hls(hass):
hass.http.register_view(HlsPlaylistView())
hass.http.register_view(HlsSegmentView())
hass.http.register_view(HlsInitView())
- return "/api/hls/{}/playlist.m3u8"
+ hass.http.register_view(HlsMasterPlaylistView())
+ return "/api/hls/{}/master_playlist.m3u8"
+
+
+class HlsMasterPlaylistView(StreamView):
+ """Stream view used only for Chromecast compatibility."""
+
+ url = r"/api/hls/{token:[a-f0-9]+}/master_playlist.m3u8"
+ name = "api:stream:hls:master_playlist"
+ cors_allowed = True
+
+ @staticmethod
+ def render(track):
+ """Render M3U8 file."""
+ # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work
+ # Calculate file size / duration and use a multiplier to account for variation
+ segment = track.get_segment(track.segments[-1])
+ bandwidth = round(
+ segment.segment.seek(0, io.SEEK_END) * 8 / segment.duration * 3
+ )
+ codecs = get_codec_string(segment.segment)
+ lines = [
+ "#EXTM3U",
+ f'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS="{codecs}"',
+ "playlist.m3u8",
+ ]
+ return "\n".join(lines) + "\n"
+
+ async def handle(self, request, stream, sequence):
+ """Return m3u8 playlist."""
+ track = stream.add_provider("hls")
+ stream.start()
+ # Wait for a segment to be ready
+ if not track.segments:
+ await track.recv()
+ headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
+ return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
class HlsPlaylistView(StreamView):
@@ -26,18 +63,50 @@ class HlsPlaylistView(StreamView):
name = "api:stream:hls:playlist"
cors_allowed = True
+ @staticmethod
+ def render_preamble(track):
+ """Render preamble."""
+ return [
+ "#EXT-X-VERSION:7",
+ f"#EXT-X-TARGETDURATION:{track.target_duration}",
+ '#EXT-X-MAP:URI="init.mp4"',
+ ]
+
+ @staticmethod
+ def render_playlist(track):
+ """Render playlist."""
+ segments = track.segments
+
+ if not segments:
+ return []
+
+ playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])]
+
+ for sequence in segments:
+ segment = track.get_segment(sequence)
+ playlist.extend(
+ [
+ "#EXTINF:{:.04f},".format(float(segment.duration)),
+ f"./segment/{segment.sequence}.m4s",
+ ]
+ )
+
+ return playlist
+
+ def render(self, track):
+ """Render M3U8 file."""
+ lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track)
+ return "\n".join(lines) + "\n"
+
async def handle(self, request, stream, sequence):
"""Return m3u8 playlist."""
- renderer = M3U8Renderer(stream)
track = stream.add_provider("hls")
stream.start()
# Wait for a segment to be ready
if not track.segments:
await track.recv()
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
- return web.Response(
- body=renderer.render(track).encode("utf-8"), headers=headers
- )
+ return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
class HlsInitView(StreamView):
@@ -77,49 +146,6 @@ async def handle(self, request, stream, sequence):
)
-class M3U8Renderer:
- """M3U8 Render Helper."""
-
- def __init__(self, stream):
- """Initialize renderer."""
- self.stream = stream
-
- @staticmethod
- def render_preamble(track):
- """Render preamble."""
- return [
- "#EXT-X-VERSION:7",
- f"#EXT-X-TARGETDURATION:{track.target_duration}",
- '#EXT-X-MAP:URI="init.mp4"',
- ]
-
- @staticmethod
- def render_playlist(track):
- """Render playlist."""
- segments = track.segments
-
- if not segments:
- return []
-
- playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])]
-
- for sequence in segments:
- segment = track.get_segment(sequence)
- playlist.extend(
- [
- "#EXTINF:{:.04f},".format(float(segment.duration)),
- f"./segment/{segment.sequence}.m4s",
- ]
- )
-
- return playlist
-
- def render(self, track):
- """Render M3U8 file."""
- lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track)
- return "\n".join(lines) + "\n"
-
-
@PROVIDERS.register("hls")
class HlsStreamOutput(StreamOutput):
"""Represents HLS Output formats."""
@@ -137,7 +163,7 @@ def format(self) -> str:
@property
def audio_codecs(self) -> str:
"""Return desired audio codecs."""
- return {"aac", "ac3", "mp3"}
+ return {"aac", "mp3"}
@property
def video_codecs(self) -> tuple:
@@ -148,7 +174,8 @@ def video_codecs(self) -> tuple:
def container_options(self) -> Callable[[int], dict]:
"""Return Callable which takes a sequence number and returns container options."""
return lambda sequence: {
- "movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont",
+ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
+ "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont",
"avoid_negative_ts": "make_non_negative",
"fragment_index": str(sequence),
}
diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index 82b146cc51f546..d0b8789f60248e 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -78,7 +78,7 @@ def format(self) -> str:
@property
def audio_codecs(self) -> str:
"""Return desired audio codec."""
- return {"aac", "ac3", "mp3"}
+ return {"aac", "mp3"}
@property
def video_codecs(self) -> tuple:
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index b76896b815a44c..22f67432a1b0ff 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -6,7 +6,7 @@
import av
-from .const import MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO
+from .const import MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO
from .core import Segment, StreamBuffer
_LOGGER = logging.getLogger(__name__)
@@ -77,6 +77,9 @@ def _stream_worker_internal(hass, stream, quit_event):
# compatible with empty_moov and manual bitstream filters not in PyAV
if container.format.name in {"hls", "mpegts"}:
audio_stream = None
+ # 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
# The presentation timestamps of the first packet in each stream we receive
# Use to adjust before muxing or outputting, but we don't adjust internally
@@ -113,7 +116,11 @@ def empty_stream_dict():
# Get to first video keyframe
while first_packet[video_stream] is None:
packet = next(container.demux())
- if packet.stream == video_stream and packet.is_keyframe:
+ if (
+ packet.stream == video_stream
+ and packet.is_keyframe
+ and packet.dts is not None
+ ):
first_packet[video_stream] = packet
initial_packets.append(packet)
# Get first_pts from subsequent frame to first keyframe
@@ -121,6 +128,8 @@ def empty_stream_dict():
[pts is None for pts in {**first_packet, **first_pts}.values()]
) and (len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO):
packet = next(container.demux((video_stream, audio_stream)))
+ if packet.dts is None:
+ continue # Discard packets with no dts
if (
first_packet[packet.stream] is None
): # actually video already found above so only for audio
@@ -191,6 +200,12 @@ def mux_audio_packet(packet):
packet.stream = output_streams[audio_stream]
buffer.output.mux(packet)
+ def finalize_stream():
+ if not stream.keepalive:
+ # End of stream, clear listeners and stop thread
+ for fmt, _ in outputs.items():
+ hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None)
+
if not peek_first_pts():
container.close()
return
@@ -213,15 +228,26 @@ def mux_audio_packet(packet):
continue
last_packet_was_without_dts = False
except (av.AVError, StopIteration) as ex:
- if not stream.keepalive:
- # End of stream, clear listeners and stop thread
- for fmt, _ in outputs.items():
- hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None)
_LOGGER.error("Error demuxing stream: %s", str(ex))
+ finalize_stream()
break
# Discard packet if dts is not monotonic
if packet.dts <= last_dts[packet.stream]:
+ if (last_dts[packet.stream] - packet.dts) > (
+ packet.time_base * MAX_TIMESTAMP_GAP
+ ):
+ _LOGGER.warning(
+ "Timestamp overflow detected: dts = %s, resetting stream",
+ packet.dts,
+ )
+ finalize_stream()
+ break
+ _LOGGER.warning(
+ "Dropping out of order packet: %s <= %s",
+ packet.dts,
+ last_dts[packet.stream],
+ )
continue
# Check for end of segment
diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py
index 5fb777bd325a2a..2d921da4a46816 100644
--- a/homeassistant/components/sun/__init__.py
+++ b/homeassistant/components/sun/__init__.py
@@ -41,7 +41,7 @@
# The algorithm used here is somewhat complicated. It aims to cut down
# the number of sensor updates over the day. It's documented best in
# the PR for the change, see the Discussion section of:
-# https://github.com/home-assistant/home-assistant/pull/23832
+# https://github.com/home-assistant/core/pull/23832
# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight
diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json
index 659a6091299dd0..2fbe4fe245f66e 100644
--- a/homeassistant/components/surepetcare/manifest.json
+++ b/homeassistant/components/surepetcare/manifest.json
@@ -3,5 +3,5 @@
"name": "Sure Petcare",
"documentation": "https://www.home-assistant.io/integrations/surepetcare",
"codeowners": ["@benleb"],
- "requirements": ["surepy==0.2.5"]
+ "requirements": ["surepy==0.2.6"]
}
diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py
index 1562a4980804b1..2c7fb483eff72a 100644
--- a/homeassistant/components/swiss_public_transport/sensor.py
+++ b/homeassistant/components/swiss_public_transport/sensor.py
@@ -103,7 +103,7 @@ def device_state_attributes(self):
self._opendata.connections[0]["departure"]
) - dt_util.as_local(dt_util.utcnow())
- attr = {
+ return {
ATTR_TRAIN_NUMBER: self._opendata.connections[0]["number"],
ATTR_PLATFORM: self._opendata.connections[0]["platform"],
ATTR_TRANSFERS: self._opendata.connections[0]["transfers"],
@@ -116,7 +116,6 @@ def device_state_attributes(self):
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_DELAY: self._opendata.connections[0]["delay"],
}
- return attr
@property
def icon(self):
diff --git a/homeassistant/components/switch/group.py b/homeassistant/components/switch/group.py
new file mode 100644
index 00000000000000..1636054663dc69
--- /dev/null
+++ b/homeassistant/components/switch/group.py
@@ -0,0 +1,15 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py
index 8b75187101427d..c9dd355392821e 100644
--- a/homeassistant/components/switch/light.py
+++ b/homeassistant/components/switch/light.py
@@ -44,18 +44,31 @@ async def async_setup_platform(
discovery_info: Optional[DiscoveryInfoType] = None,
) -> None:
"""Initialize Light Switch platform."""
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ wrapped_switch = registry.async_get(config[CONF_ENTITY_ID])
+ unique_id = wrapped_switch.unique_id if wrapped_switch else None
+
async_add_entities(
- [LightSwitch(cast(str, config.get(CONF_NAME)), config[CONF_ENTITY_ID])], True
+ [
+ LightSwitch(
+ cast(str, config.get(CONF_NAME)),
+ config[CONF_ENTITY_ID],
+ unique_id,
+ )
+ ],
+ True,
)
class LightSwitch(LightEntity):
"""Represents a Switch as a Light."""
- def __init__(self, name: str, switch_entity_id: str) -> None:
+ def __init__(self, name: str, switch_entity_id: str, unique_id: str) -> None:
"""Initialize Light Switch."""
self._name = name
self._switch_entity_id = switch_entity_id
+ self._unique_id = unique_id
self._is_on = False
self._available = False
self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None
@@ -80,6 +93,11 @@ def should_poll(self) -> bool:
"""No polling needed for a light switch."""
return False
+ @property
+ def unique_id(self):
+ """Return the unique id of the light switch."""
+ return self._unique_id
+
async def async_turn_on(self, **kwargs):
"""Forward the turn_on command to the switch in this light switch."""
data = {ATTR_ENTITY_ID: self._switch_entity_id}
diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json
index d992df0421f3ec..d68938ddda0f16 100644
--- a/homeassistant/components/switch/translations/et.json
+++ b/homeassistant/components/switch/translations/et.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Muuda {entity_name} olekut",
+ "turn_off": "L\u00fclita {entity_name} v\u00e4lja",
+ "turn_on": "L\u00fclita {entity_name} sisse"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud",
+ "is_on": "{entity_name} on sisse l\u00fclitatud"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
+ }
+ },
"state": {
"_": {
"off": "V\u00e4ljas",
diff --git a/homeassistant/components/switch/translations/uk.json b/homeassistant/components/switch/translations/uk.json
index 7ac96bd7039059..bee9eb957d530f 100644
--- a/homeassistant/components/switch/translations/uk.json
+++ b/homeassistant/components/switch/translations/uk.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
diff --git a/homeassistant/components/syncthru/strings.json b/homeassistant/components/syncthru/strings.json
index 1824763c8f8f6c..0164fdf6ddc2fe 100644
--- a/homeassistant/components/syncthru/strings.json
+++ b/homeassistant/components/syncthru/strings.json
@@ -18,7 +18,7 @@
},
"user": {
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"url": "Web interface URL"
}
}
diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json
index 76eaf5f21f3c45..8e568131e6203f 100644
--- a/homeassistant/components/syncthru/translations/de.json
+++ b/homeassistant/components/syncthru/translations/de.json
@@ -8,6 +8,7 @@
"syncthru_not_supported": "Ger\u00e4t unterst\u00fctzt kein SyncThru",
"unknown_state": "Druckerstatus unbekannt, \u00fcberpr\u00fcfe URL und Netzwerkverbindung"
},
+ "flow_title": "Samsung SyncThru Drucker: {name}",
"step": {
"confirm": {
"data": {
diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/syncthru/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/translations/pl.json b/homeassistant/components/syncthru/translations/pl.json
index bd174d000c8b2a..ac9a511ba3270a 100644
--- a/homeassistant/components/syncthru/translations/pl.json
+++ b/homeassistant/components/syncthru/translations/pl.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"invalid_url": "Nieprawid\u0142owy URL",
+ "syncthru_not_supported": "Urz\u0105dzenie nie obs\u0142uguje SyncThru",
"unknown_state": "Nieznany stan drukarki, sprawd\u017a adres URL i \u0142\u0105czno\u015b\u0107 sieciow\u0105"
},
"flow_title": "Drukarka Samsung SyncThru: {name}"
diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py
index 5a619c821dce2e..4417f72918d0dc 100644
--- a/homeassistant/components/synology/camera.py
+++ b/homeassistant/components/synology/camera.py
@@ -42,6 +42,13 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Synology IP Camera."""
+ _LOGGER.warning(
+ "The Synology integration is deprecated."
+ " Please use the Synology DSM integration"
+ " (https://www.home-assistant.io/integrations/synology_dsm/) instead."
+ " This integration will be removed in version 0.118.0."
+ )
+
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py
index c8f665cb408c09..df43c5668f388a 100644
--- a/homeassistant/components/synology_chat/notify.py
+++ b/homeassistant/components/synology_chat/notify.py
@@ -10,7 +10,7 @@
PLATFORM_SCHEMA,
BaseNotificationService,
)
-from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL, HTTP_OK
+from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL, HTTP_CREATED, HTTP_OK
import homeassistant.helpers.config_validation as cv
ATTR_FILE_URL = "file_url"
@@ -57,7 +57,7 @@ def send_message(self, message="", **kwargs):
self._resource, data=to_send, timeout=10, verify=self._verify_ssl
)
- if response.status_code not in (HTTP_OK, 201):
+ if response.status_code not in (HTTP_OK, HTTP_CREATED):
_LOGGER.exception(
"Error sending message. Response %d: %s:",
response.status_code,
diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py
index 223235a41215a2..7f6cef46cbcedd 100644
--- a/homeassistant/components/synology_dsm/__init__.py
+++ b/homeassistant/components/synology_dsm/__init__.py
@@ -23,6 +23,7 @@
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import callback
@@ -233,6 +234,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry):
self._with_security = True
self._with_storage = True
self._with_utilisation = True
+ self._with_information = True
self._with_surveillance_station = True
self._unsub_dispatcher = None
@@ -250,6 +252,7 @@ async def async_setup(self):
self._entry.data[CONF_USERNAME],
self._entry.data[CONF_PASSWORD],
self._entry.data[CONF_SSL],
+ timeout=self._entry.options.get(CONF_TIMEOUT),
device_token=self._entry.data.get("device_token"),
)
@@ -302,11 +305,14 @@ def _async_setup_api_requests(self):
self._with_utilisation = bool(
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
)
+ self._with_information = bool(
+ self._fetching_entities.get(SynoDSMInformation.API_KEY)
+ )
self._with_surveillance_station = bool(
self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY)
)
- # Reset not used API
+ # Reset not used API, information is not reset since it's used in device_info
if not self._with_security:
self.dsm.reset(self.security)
self.security = None
@@ -349,7 +355,7 @@ async def async_unload(self):
async def async_update(self, now=None):
"""Update function for updating API information."""
self._async_setup_api_requests()
- await self._hass.async_add_executor_job(self.dsm.update)
+ await self._hass.async_add_executor_job(self.dsm.update, self._with_information)
async_dispatcher_send(self._hass, self.signal_sensor_update)
diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py
index a75f57db678f6a..6c11d31b7f7c9c 100644
--- a/homeassistant/components/synology_dsm/binary_sensor.py
+++ b/homeassistant/components/synology_dsm/binary_sensor.py
@@ -67,7 +67,4 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return the state."""
- attr = getattr(self._api.storage, self.entity_type)(self._device_id)
- if attr is None:
- return None
- return attr
+ return getattr(self._api.storage, self.entity_type)(self._device_id)
diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py
index 4cb34ee04315ad..80e6802e4435fd 100644
--- a/homeassistant/components/synology_dsm/camera.py
+++ b/homeassistant/components/synology_dsm/camera.py
@@ -60,9 +60,13 @@ def device_info(self) -> Dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._api.information.serial, self._camera.id)},
- "name": self.name,
+ "name": self._camera.name,
"model": self._camera.model,
- "via_device": (DOMAIN, self._api.information.serial),
+ "via_device": (
+ DOMAIN,
+ self._api.information.serial,
+ SynoSurveillanceStation.INFO_API_KEY,
+ ),
}
@property
diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py
index a9df6f362cc9a7..35efb253ed1d73 100644
--- a/homeassistant/components/synology_dsm/config_flow.py
+++ b/homeassistant/components/synology_dsm/config_flow.py
@@ -23,6 +23,7 @@
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import callback
@@ -34,6 +35,7 @@
DEFAULT_PORT_SSL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SSL,
+ DEFAULT_TIMEOUT,
)
from .const import DOMAIN # pylint: disable=unused-import
@@ -138,10 +140,10 @@ async def async_step_user(self, user_input=None):
return await self.async_step_2sa(user_input, errors)
except SynologyDSMLoginInvalidException as ex:
_LOGGER.error(ex)
- errors[CONF_USERNAME] = "login"
+ errors[CONF_USERNAME] = "invalid_auth"
except SynologyDSMRequestException as ex:
_LOGGER.error(ex)
- errors[CONF_HOST] = "connection"
+ errors[CONF_HOST] = "cannot_connect"
except SynologyDSMException as ex:
_LOGGER.error(ex)
errors["base"] = "unknown"
@@ -250,7 +252,13 @@ async def async_step_init(self, user_input=None):
default=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
- ): cv.positive_int
+ ): cv.positive_int,
+ vol.Optional(
+ CONF_TIMEOUT,
+ default=self.config_entry.options.get(
+ CONF_TIMEOUT, DEFAULT_TIMEOUT
+ ),
+ ): cv.positive_int,
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py
index 693d8b2cd50301..82bb232461e201 100644
--- a/homeassistant/components/synology_dsm/const.py
+++ b/homeassistant/components/synology_dsm/const.py
@@ -2,18 +2,22 @@
from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.utilization import SynoCoreUtilization
+from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.storage.storage import SynoStorage
+from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY
from homeassistant.const import (
DATA_MEGABYTES,
DATA_RATE_KILOBYTES_PER_SECOND,
DATA_TERABYTES,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
PERCENTAGE,
)
DOMAIN = "synology_dsm"
-PLATFORMS = ["binary_sensor", "camera", "sensor"]
+PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"]
# Entry keys
SYNO_API = "syno_api"
@@ -27,6 +31,7 @@
DEFAULT_PORT_SSL = 5001
# Options
DEFAULT_SCAN_INTERVAL = 15 # min
+DEFAULT_TIMEOUT = 10 # sec
ENTITY_NAME = "name"
@@ -38,26 +43,26 @@
# Entity keys should start with the API_KEY to fetch
# Binary sensors
-STORAGE_DISK_BINARY_SENSORS = {
- f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": {
- ENTITY_NAME: "Exceeded Max Bad Sectors",
+SECURITY_BINARY_SENSORS = {
+ f"{SynoCoreSecurity.API_KEY}:status": {
+ ENTITY_NAME: "Security status",
ENTITY_UNIT: None,
ENTITY_ICON: None,
ENTITY_CLASS: DEVICE_CLASS_SAFETY,
ENTITY_ENABLE: True,
},
- f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": {
- ENTITY_NAME: "Below Min Remaining Life",
+}
+
+STORAGE_DISK_BINARY_SENSORS = {
+ f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": {
+ ENTITY_NAME: "Exceeded Max Bad Sectors",
ENTITY_UNIT: None,
ENTITY_ICON: None,
ENTITY_CLASS: DEVICE_CLASS_SAFETY,
ENTITY_ENABLE: True,
},
-}
-
-SECURITY_BINARY_SENSORS = {
- f"{SynoCoreSecurity.API_KEY}:status": {
- ENTITY_NAME: "Security status",
+ f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": {
+ ENTITY_NAME: "Below Min Remaining Life",
ENTITY_UNIT: None,
ENTITY_ICON: None,
ENTITY_CLASS: DEVICE_CLASS_SAFETY,
@@ -212,15 +217,15 @@
f"{SynoStorage.API_KEY}:volume_disk_temp_avg": {
ENTITY_NAME: "Average Disk Temp",
ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:thermometer",
- ENTITY_CLASS: "temperature",
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE,
ENTITY_ENABLE: True,
},
f"{SynoStorage.API_KEY}:volume_disk_temp_max": {
ENTITY_NAME: "Maximum Disk Temp",
ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:thermometer",
- ENTITY_CLASS: "temperature",
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE,
ENTITY_ENABLE: False,
},
}
@@ -242,11 +247,44 @@
f"{SynoStorage.API_KEY}:disk_temp": {
ENTITY_NAME: "Temperature",
ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:thermometer",
- ENTITY_CLASS: "temperature",
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ENTITY_ENABLE: True,
+ },
+}
+
+INFORMATION_SENSORS = {
+ f"{SynoDSMInformation.API_KEY}:temperature": {
+ ENTITY_NAME: "temperature",
+ ENTITY_UNIT: None,
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ENTITY_ENABLE: True,
+ },
+ f"{SynoDSMInformation.API_KEY}:uptime": {
+ ENTITY_NAME: "last boot",
+ ENTITY_UNIT: None,
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_TIMESTAMP,
+ ENTITY_ENABLE: False,
+ },
+}
+
+# Switch
+SURVEILLANCE_SWITCH = {
+ f"{SynoSurveillanceStation.HOME_MODE_API_KEY}:home_mode": {
+ ENTITY_NAME: "home mode",
+ ENTITY_UNIT: None,
+ ENTITY_ICON: "mdi:home-account",
+ ENTITY_CLASS: None,
ENTITY_ENABLE: True,
},
}
-TEMP_SENSORS_KEYS = ["volume_disk_temp_avg", "volume_disk_temp_max", "disk_temp"]
+TEMP_SENSORS_KEYS = [
+ "volume_disk_temp_avg",
+ "volume_disk_temp_max",
+ "disk_temp",
+ "temperature",
+]
diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py
index 22171fdf2f59ef..31013451682570 100644
--- a/homeassistant/components/synology_dsm/sensor.py
+++ b/homeassistant/components/synology_dsm/sensor.py
@@ -1,4 +1,7 @@
"""Support for Synology DSM sensors."""
+from datetime import timedelta
+from typing import Dict
+
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DISKS,
@@ -10,11 +13,13 @@
)
from homeassistant.helpers.temperature import display_temp
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util.dt import utcnow
-from . import SynologyDSMDeviceEntity, SynologyDSMEntity
+from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMEntity
from .const import (
CONF_VOLUMES,
DOMAIN,
+ INFORMATION_SENSORS,
STORAGE_DISK_SENSORS,
STORAGE_VOL_SENSORS,
SYNO_API,
@@ -55,6 +60,11 @@ async def async_setup_entry(
for sensor_type in STORAGE_DISK_SENSORS
]
+ entities += [
+ SynoDSMInfoSensor(api, sensor_type, INFORMATION_SENSORS[sensor_type])
+ for sensor_type in INFORMATION_SENSORS
+ ]
+
async_add_entities(entities)
@@ -105,3 +115,34 @@ def state(self):
return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS)
return attr
+
+
+class SynoDSMInfoSensor(SynologyDSMEntity):
+ """Representation a Synology information sensor."""
+
+ def __init__(self, api: SynoApi, entity_type: str, entity_info: Dict[str, str]):
+ """Initialize the Synology SynoDSMInfoSensor entity."""
+ super().__init__(api, entity_type, entity_info)
+ self._previous_uptime = None
+ self._last_boot = None
+
+ @property
+ def state(self):
+ """Return the state."""
+ attr = getattr(self._api.information, self.entity_type)
+ if attr is None:
+ return None
+
+ # Temperature
+ if self.entity_type in TEMP_SENSORS_KEYS:
+ return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS)
+
+ if self.entity_type == "uptime":
+ # reboot happened or entity creation
+ if self._previous_uptime is None or self._previous_uptime > attr:
+ last_boot = utcnow() - timedelta(seconds=attr)
+ self._last_boot = last_boot.replace(microsecond=0).isoformat()
+
+ self._previous_uptime = attr
+ return self._last_boot
+ return attr
diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json
index c46f645719f2ad..9ff0b16f8fb125 100644
--- a/homeassistant/components/synology_dsm/strings.json
+++ b/homeassistant/components/synology_dsm/strings.json
@@ -7,7 +7,7 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
- "ssl": "Use SSL/TLS to connect to your NAS",
+ "ssl": "[%key:common::config_flow::data::ssl%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
@@ -22,7 +22,7 @@
"title": "Synology DSM",
"description": "Do you want to setup {name} ({host})?",
"data": {
- "ssl": "Use SSL/TLS to connect to your NAS",
+ "ssl": "[%key:common::config_flow::data::ssl%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]"
@@ -30,21 +30,22 @@
}
},
"error": {
- "connection": "Connection error: please check your host, port & ssl",
- "login": "Login error: please check your username & password",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"missing_data": "Missing data: please retry later or an other configuration",
"otp_failed": "Two-step authentication failed, retry with a new pass code",
- "unknown": "Unknown error: please check logs to get more details"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "Host already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"data": {
- "scan_interval": "Minutes between scans"
+ "scan_interval": "Minutes between scans",
+ "timeout": "Timeout (seconds)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py
new file mode 100644
index 00000000000000..ee29c9f2692801
--- /dev/null
+++ b/homeassistant/components/synology_dsm/switch.py
@@ -0,0 +1,98 @@
+"""Support for Synology DSM switch."""
+from typing import Dict
+
+from synology_dsm.api.surveillance_station import SynoSurveillanceStation
+
+from homeassistant.components.switch import ToggleEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import SynoApi, SynologyDSMEntity
+from .const import DOMAIN, SURVEILLANCE_SWITCH, SYNO_API
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Synology NAS switch."""
+
+ api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+
+ entities = []
+
+ if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis:
+ info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info)
+ version = info["data"]["CMSMinVersion"]
+ entities += [
+ SynoDSMSurveillanceHomeModeToggle(
+ api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version
+ )
+ for sensor_type in SURVEILLANCE_SWITCH
+ ]
+
+ async_add_entities(entities, True)
+
+
+class SynoDSMSurveillanceHomeModeToggle(SynologyDSMEntity, ToggleEntity):
+ """Representation a Synology Surveillance Station Home Mode toggle."""
+
+ def __init__(
+ self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], version: str
+ ):
+ """Initialize a Synology Surveillance Station Home Mode."""
+ super().__init__(
+ api,
+ entity_type,
+ entity_info,
+ )
+ self._version = version
+ self._state = None
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state."""
+ if self.entity_type == "home_mode":
+ return self._state
+ return None
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return True
+
+ async def async_update(self):
+ """Update the toggle state."""
+ self._state = await self.hass.async_add_executor_job(
+ self._api.surveillance_station.get_home_mode_status
+ )
+
+ def turn_on(self, **kwargs) -> None:
+ """Turn on Home mode."""
+ self._api.surveillance_station.set_home_mode(True)
+
+ def turn_off(self, **kwargs) -> None:
+ """Turn off Home mode."""
+ self._api.surveillance_station.set_home_mode(False)
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return bool(self._api.surveillance_station)
+
+ @property
+ def device_info(self) -> Dict[str, any]:
+ """Return the device information."""
+ return {
+ "identifiers": {
+ (
+ DOMAIN,
+ self._api.information.serial,
+ SynoSurveillanceStation.INFO_API_KEY,
+ )
+ },
+ "name": "Surveillance Station",
+ "manufacturer": "Synology",
+ "model": self._api.information.model,
+ "sw_version": self._version,
+ "via_device": (DOMAIN, self._api.information.serial),
+ }
diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json
index fef0bca2ce4e64..a635d4dcd5f95f 100644
--- a/homeassistant/components/synology_dsm/translations/ca.json
+++ b/homeassistant/components/synology_dsm/translations/ca.json
@@ -22,7 +22,7 @@
"data": {
"password": "Contrasenya",
"port": "Port",
- "ssl": "Utilitza SSL/TLS per connectar-te al servidor NAS",
+ "ssl": "Utilitza un certificat SSL",
"username": "Nom d'usuari"
},
"description": "Vols configurar {name} ({host})?",
@@ -33,7 +33,7 @@
"host": "Amfitri\u00f3",
"password": "Contrasenya",
"port": "Port",
- "ssl": "Utilitza SSL/TLS per connectar-te al servidor NAS",
+ "ssl": "Utilitza un certificat SSL",
"username": "Nom d'usuari"
},
"title": "Synology DSM"
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minuts entre escanejos"
+ "scan_interval": "Minuts entre escanejos",
+ "timeout": "Temps d'espera (segons)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json
index 57dc028c0f4c37..d1f593c7938caf 100644
--- a/homeassistant/components/synology_dsm/translations/cs.json
+++ b/homeassistant/components/synology_dsm/translations/cs.json
@@ -17,5 +17,14 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u010casov\u00fd limit (v sekund\u00e1ch)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/el.json b/homeassistant/components/synology_dsm/translations/el.json
new file mode 100644
index 00000000000000..e23b1fe0cf6363
--- /dev/null
+++ b/homeassistant/components/synology_dsm/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json
index 48a8118528a04a..1a2fc17b35163f 100644
--- a/homeassistant/components/synology_dsm/translations/en.json
+++ b/homeassistant/components/synology_dsm/translations/en.json
@@ -22,7 +22,7 @@
"data": {
"password": "Password",
"port": "Port",
- "ssl": "Use SSL/TLS to connect to your NAS",
+ "ssl": "Uses an SSL certificate",
"username": "Username"
},
"description": "Do you want to setup {name} ({host})?",
@@ -33,7 +33,7 @@
"host": "Host",
"password": "Password",
"port": "Port",
- "ssl": "Use SSL/TLS to connect to your NAS",
+ "ssl": "Uses an SSL certificate",
"username": "Username"
},
"title": "Synology DSM"
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minutes between scans"
+ "scan_interval": "Minutes between scans",
+ "timeout": "Timeout (seconds)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json
index a498333e049e7a..ca02cf41e23213 100644
--- a/homeassistant/components/synology_dsm/translations/es.json
+++ b/homeassistant/components/synology_dsm/translations/es.json
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minutos entre escaneos"
+ "scan_interval": "Minutos entre escaneos",
+ "timeout": "Tiempo de espera (segundos)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json
new file mode 100644
index 00000000000000..084f1e53b08edf
--- /dev/null
+++ b/homeassistant/components/synology_dsm/translations/et.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Ajal\u00f5pp (sekundites)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json
index 6c8b627a76bcb0..1c411591f1aeb5 100644
--- a/homeassistant/components/synology_dsm/translations/fr.json
+++ b/homeassistant/components/synology_dsm/translations/fr.json
@@ -39,5 +39,15 @@
"title": "Synology DSM"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Minutes entre les scans",
+ "timeout": "D\u00e9lai d'expiration (secondes)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json
index 1bb6dc026d8b5a..4ab4bc5aac48ed 100644
--- a/homeassistant/components/synology_dsm/translations/it.json
+++ b/homeassistant/components/synology_dsm/translations/it.json
@@ -22,7 +22,7 @@
"data": {
"password": "Password",
"port": "Porta",
- "ssl": "Utilizzare SSL/TLS per connettersi al NAS",
+ "ssl": "Utilizza un certificato SSL",
"username": "Nome utente"
},
"description": "Vuoi impostare {name} ({host})?",
@@ -33,7 +33,7 @@
"host": "Host",
"password": "Password",
"port": "Porta",
- "ssl": "Utilizzare SSL/TLS per connettersi al NAS",
+ "ssl": "Utilizza un certificato SSL",
"username": "Nome utente"
},
"title": "Synology DSM"
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minuti tra una scansione e l'altra"
+ "scan_interval": "Minuti tra una scansione e l'altra",
+ "timeout": "Timeout (in secondi)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json
index 81b7d5f143596a..6c0dc98b4ae1f0 100644
--- a/homeassistant/components/synology_dsm/translations/ko.json
+++ b/homeassistant/components/synology_dsm/translations/ko.json
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)"
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)",
+ "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/lb.json b/homeassistant/components/synology_dsm/translations/lb.json
index 9ca8d0cdfa55d3..63e1be3fa2d019 100644
--- a/homeassistant/components/synology_dsm/translations/lb.json
+++ b/homeassistant/components/synology_dsm/translations/lb.json
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minutte t\u00ebscht Scannen"
+ "scan_interval": "Minutte t\u00ebscht Scannen",
+ "timeout": "Z\u00e4itiwwerscheidung (sekonnen)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json
index 5798dce567d319..ee7c89f7192855 100644
--- a/homeassistant/components/synology_dsm/translations/nl.json
+++ b/homeassistant/components/synology_dsm/translations/nl.json
@@ -39,5 +39,14 @@
"title": "Synology DSM"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Time-out (seconden)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json
index f8d7add4dc2b55..38eb87793aeec8 100644
--- a/homeassistant/components/synology_dsm/translations/no.json
+++ b/homeassistant/components/synology_dsm/translations/no.json
@@ -22,7 +22,7 @@
"data": {
"password": "Passord",
"port": "",
- "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en",
+ "ssl": "Bruker et SSL-sertifikat",
"username": "Brukernavn"
},
"description": "Vil du konfigurere {name} ({host})?",
@@ -33,7 +33,7 @@
"host": "Vert",
"password": "Passord",
"port": "",
- "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en",
+ "ssl": "Bruker et SSL-sertifikat",
"username": "Brukernavn"
},
"title": ""
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minutter mellom skanninger"
+ "scan_interval": "Minutter mellom skanninger",
+ "timeout": "Tidsavbrudd (sekunder)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json
index 60c7ee849f1015..6c39999022d6cc 100644
--- a/homeassistant/components/synology_dsm/translations/pl.json
+++ b/homeassistant/components/synology_dsm/translations/pl.json
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji [min]"
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji [min]",
+ "timeout": "Limit czasu (sekundy)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json
index c0e5da7a1f1f4a..d1d65a79b68bae 100644
--- a/homeassistant/components/synology_dsm/translations/ru.json
+++ b/homeassistant/components/synology_dsm/translations/ru.json
@@ -22,7 +22,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"username": "\u041b\u043e\u0433\u0438\u043d"
},
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?",
@@ -33,7 +33,7 @@
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"username": "\u041b\u043e\u0433\u0438\u043d"
},
"title": "Synology DSM"
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u043c\u0438\u043d.)"
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u043c\u0438\u043d.)",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json
index 6aaee8b44aaa8b..690e622ecd2ebe 100644
--- a/homeassistant/components/synology_dsm/translations/sv.json
+++ b/homeassistant/components/synology_dsm/translations/sv.json
@@ -29,7 +29,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "Minuter mellan skanningar"
+ "scan_interval": "Minuter mellan skanningar",
+ "timeout": "Timeout (sekunder)"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json
index eec37c812f8981..57f79054b30ed1 100644
--- a/homeassistant/components/synology_dsm/translations/zh-Hant.json
+++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json
@@ -22,7 +22,7 @@
"data": {
"password": "\u5bc6\u78bc",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 NAS",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
},
"description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f",
@@ -33,7 +33,7 @@
"host": "\u4e3b\u6a5f\u7aef",
"password": "\u5bc6\u78bc",
"port": "\u901a\u8a0a\u57e0",
- "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 NAS",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
},
"title": "\u7fa4\u6689 DSM"
@@ -44,7 +44,8 @@
"step": {
"init": {
"data": {
- "scan_interval": "\u6383\u63cf\u9593\u9694\u5206\u6578"
+ "scan_interval": "\u6383\u63cf\u9593\u9694\u5206\u6578",
+ "timeout": "\u903e\u6642\uff08\u79d2\uff09"
}
}
}
diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
index b8d6b1664ac7ef..bb255ba8bf38ef 100644
--- a/homeassistant/components/system_log/__init__.py
+++ b/homeassistant/components/system_log/__init__.py
@@ -211,6 +211,8 @@ async def async_setup(hass, config):
handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT])
+ hass.data[DOMAIN] = handler
+
listener = logging.handlers.QueueListener(
simple_queue, handler, respect_handler_level=True
)
@@ -222,6 +224,7 @@ def _async_stop_queue_handler(_) -> None:
"""Cleanup handler."""
logging.root.removeHandler(queue_handler)
listener.stop()
+ del hass.data[DOMAIN]
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler)
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index e7ad77e1842c24..e29081257e056e 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -87,7 +87,7 @@
None,
False,
],
- "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, True],
+ "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False],
"swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False],
"swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False],
}
diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json
index f0f2ce4ab99db1..e0eb90f7ddc95f 100644
--- a/homeassistant/components/tado/strings.json
+++ b/homeassistant/components/tado/strings.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"step": {
"user": {
@@ -13,10 +13,10 @@
}
},
"error": {
- "unknown": "Unexpected error",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
"no_homes": "There are no homes linked to this tado account.",
- "invalid_auth": "Invalid authentication",
- "cannot_connect": "Failed to connect, please try again"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
@@ -30,4 +30,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/tado/translations/ca.json b/homeassistant/components/tado/translations/ca.json
index df340784f7fc19..935b58483d7344 100644
--- a/homeassistant/components/tado/translations/ca.json
+++ b/homeassistant/components/tado/translations/ca.json
@@ -4,7 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar, torna-ho a provar",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"no_homes": "No hi ha cases enlla\u00e7ades a aquest compte Tado.",
"unknown": "Error inesperat"
diff --git a/homeassistant/components/tado/translations/en.json b/homeassistant/components/tado/translations/en.json
index d6f28c43cb215d..4e0df459437cca 100644
--- a/homeassistant/components/tado/translations/en.json
+++ b/homeassistant/components/tado/translations/en.json
@@ -4,7 +4,7 @@
"already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect, please try again",
+ "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"no_homes": "There are no homes linked to this tado account.",
"unknown": "Unexpected error"
diff --git a/homeassistant/components/tado/translations/et.json b/homeassistant/components/tado/translations/et.json
new file mode 100644
index 00000000000000..0d70cd06fcaa70
--- /dev/null
+++ b/homeassistant/components/tado/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "unknown": "Ootamatu t\u00f5rge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json
index 18196a4bf13f47..0ebbe4054a183c 100644
--- a/homeassistant/components/tado/translations/fr.json
+++ b/homeassistant/components/tado/translations/fr.json
@@ -25,6 +25,7 @@
"data": {
"fallback": "Activer le mode restreint."
},
+ "description": "Le mode de repli passera au programme intelligent au prochain changement de programme apr\u00e8s avoir ajust\u00e9 manuellement une zone.",
"title": "Ajustez les options de Tado."
}
}
diff --git a/homeassistant/components/tado/translations/it.json b/homeassistant/components/tado/translations/it.json
index 775ef702b84971..2c09ca743127d7 100644
--- a/homeassistant/components/tado/translations/it.json
+++ b/homeassistant/components/tado/translations/it.json
@@ -4,7 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi, si prega di riprovare",
+ "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"no_homes": "Non ci sono case collegate a questo account tado.",
"unknown": "Errore imprevisto"
diff --git a/homeassistant/components/tado/translations/no.json b/homeassistant/components/tado/translations/no.json
index 594084c6571a6f..0ac5518709cfff 100644
--- a/homeassistant/components/tado/translations/no.json
+++ b/homeassistant/components/tado/translations/no.json
@@ -4,7 +4,7 @@
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"no_homes": "Det er ingen hjem knyttet til denne tado-kontoen.",
"unknown": "Uventet feil"
diff --git a/homeassistant/components/tado/translations/pl.json b/homeassistant/components/tado/translations/pl.json
index f95374e0329621..46c78ce438ed68 100644
--- a/homeassistant/components/tado/translations/pl.json
+++ b/homeassistant/components/tado/translations/pl.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"no_homes": "Brak dom\u00f3w powi\u0105zanych z tym kontem Tado.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/tado/translations/ru.json b/homeassistant/components/tado/translations/ru.json
index 04d1a0d9545797..8ffb14edc0efc2 100644
--- a/homeassistant/components/tado/translations/ru.json
+++ b/homeassistant/components/tado/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"no_homes": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0434\u043e\u043c\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
diff --git a/homeassistant/components/tado/translations/zh-Hant.json b/homeassistant/components/tado/translations/zh-Hant.json
index 911520065fce9f..59e2d80c561f4b 100644
--- a/homeassistant/components/tado/translations/zh-Hant.json
+++ b/homeassistant/components/tado/translations/zh-Hant.json
@@ -4,7 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"no_homes": "\u6b64 Tado \u5e33\u865f\u672a\u7d81\u5b9a\u4efb\u4f55\u5bb6\u5ead\u3002",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py
index 872d097d5dea22..321dce9a296b38 100644
--- a/homeassistant/components/tag/__init__.py
+++ b/homeassistant/components/tag/__init__.py
@@ -70,7 +70,7 @@ async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
data[TAG_ID] = str(uuid.uuid4())
# make last_scanned JSON serializeable
if LAST_SCANNED in data:
- data[LAST_SCANNED] = str(data[LAST_SCANNED])
+ data[LAST_SCANNED] = data[LAST_SCANNED].isoformat()
return data
@callback
@@ -82,8 +82,8 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic
"""Return a new updated data object."""
data = {**data, **self.UPDATE_SCHEMA(update_data)}
# make last_scanned JSON serializeable
- if LAST_SCANNED in data:
- data[LAST_SCANNED] = str(data[LAST_SCANNED])
+ if LAST_SCANNED in update_data:
+ data[LAST_SCANNED] = data[LAST_SCANNED].isoformat()
return data
@@ -100,6 +100,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
+
return True
diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json
index d330fdaf3f8f84..f2d3a4133b89e4 100644
--- a/homeassistant/components/tag/manifest.json
+++ b/homeassistant/components/tag/manifest.json
@@ -1,12 +1,7 @@
{
"domain": "tag",
- "name": "Tag",
- "config_flow": false,
+ "name": "Tags",
"documentation": "https://www.home-assistant.io/integrations/tag",
- "requirements": [],
- "ssdp": [],
- "zeroconf": [],
- "homekit": {},
- "dependencies": [],
- "codeowners": ["@balloob", "@dmulcahey"]
+ "codeowners": ["@balloob", "@dmulcahey"],
+ "quality_scale": "internal"
}
diff --git a/homeassistant/components/tag/translations/ko.json b/homeassistant/components/tag/translations/ko.json
new file mode 100644
index 00000000000000..8cee64dc465573
--- /dev/null
+++ b/homeassistant/components/tag/translations/ko.json
@@ -0,0 +1,3 @@
+{
+ "title": "\ud0dc\uadf8"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/translations/pl.json b/homeassistant/components/tag/translations/pl.json
new file mode 100644
index 00000000000000..fdac700612daf4
--- /dev/null
+++ b/homeassistant/components/tag/translations/pl.json
@@ -0,0 +1,3 @@
+{
+ "title": "Tag"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py
index 39e492601bd83e..af06bf5ca4c77f 100644
--- a/homeassistant/components/tahoma/binary_sensor.py
+++ b/homeassistant/components/tahoma/binary_sensor.py
@@ -2,7 +2,10 @@
from datetime import timedelta
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_SMOKE,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice
@@ -45,7 +48,7 @@ def is_on(self):
def device_class(self):
"""Return the class of the device."""
if self.tahoma_device.type == "rtds:RTDSSmokeSensor":
- return "smoke"
+ return DEVICE_CLASS_SMOKE
return None
@property
diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py
index 7b28989ad8e96e..fb1129cfa0edc1 100644
--- a/homeassistant/components/tahoma/sensor.py
+++ b/homeassistant/components/tahoma/sensor.py
@@ -2,7 +2,7 @@
from datetime import timedelta
import logging
-from homeassistant.const import ATTR_BATTERY_LEVEL, PERCENTAGE, TEMP_CELSIUS
+from homeassistant.const import ATTR_BATTERY_LEVEL, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice
@@ -49,7 +49,7 @@ def unit_of_measurement(self):
if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor":
return None
if self.tahoma_device.type == "io:LightIOSystemSensor":
- return "lx"
+ return LIGHT_LUX
if self.tahoma_device.type == "Humidity Sensor":
return PERCENTAGE
if self.tahoma_device.type == "rtds:RTDSContactSensor":
diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py
index b0dd3368ad3a89..6985072b065e30 100644
--- a/homeassistant/components/tankerkoenig/sensor.py
+++ b/homeassistant/components/tankerkoenig/sensor.py
@@ -2,7 +2,12 @@
import logging
-from homeassistant.const import ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ CURRENCY_EURO,
+)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -106,7 +111,7 @@ def icon(self):
@property
def unit_of_measurement(self):
"""Return unit of measurement."""
- return "€"
+ return CURRENCY_EURO
@property
def state(self):
diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py
new file mode 100644
index 00000000000000..1e11aff448e7f1
--- /dev/null
+++ b/homeassistant/components/tasmota/__init__.py
@@ -0,0 +1,104 @@
+"""The Tasmota integration."""
+import logging
+
+from hatasmota.const import (
+ CONF_MAC,
+ CONF_MANUFACTURER,
+ CONF_MODEL,
+ CONF_NAME,
+ CONF_SW_VERSION,
+)
+from hatasmota.discovery import clear_discovery_topic
+from hatasmota.mqtt import TasmotaMQTTClient
+
+from homeassistant.components import mqtt
+from homeassistant.components.mqtt.subscription import (
+ async_subscribe_topics,
+ async_unsubscribe_topics,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import discovery
+from .const import CONF_DISCOVERY_PREFIX
+from .discovery import TASMOTA_DISCOVERY_DEVICE
+
+_LOGGER = logging.getLogger(__name__)
+
+DEVICE_MACS = "tasmota_devices"
+
+
+async def async_setup(hass: HomeAssistantType, config: dict):
+ """Set up the Tasmota component."""
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Tasmota from a config entry."""
+ hass.data[DEVICE_MACS] = {}
+
+ def _publish(*args, **kwds):
+ mqtt.async_publish(hass, *args, **kwds)
+
+ async def _subscribe_topics(sub_state, topics):
+ # Optionally mark message handlers as callback
+ for topic in topics.values():
+ if "msg_callback" in topic and "event_loop_safe" in topic:
+ topic["msg_callback"] = callback(topic["msg_callback"])
+ return await async_subscribe_topics(hass, sub_state, topics)
+
+ async def _unsubscribe_topics(sub_state):
+ return await async_unsubscribe_topics(hass, sub_state)
+
+ tasmota_mqtt = TasmotaMQTTClient(_publish, _subscribe_topics, _unsubscribe_topics)
+
+ discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX]
+ await discovery.async_start(hass, discovery_prefix, entry, tasmota_mqtt)
+
+ async def async_discover_device(config, mac):
+ """Discover and add a Tasmota device."""
+ await async_setup_device(hass, mac, config, entry, tasmota_mqtt)
+
+ async_dispatcher_connect(hass, TASMOTA_DISCOVERY_DEVICE, async_discover_device)
+
+ return True
+
+
+async def _remove_device(hass, config_entry, mac, tasmota_mqtt):
+ """Remove device from device registry."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)})
+
+ if device is None:
+ return
+
+ _LOGGER.debug("Removing tasmota device %s", mac)
+ device_registry.async_remove_device(device.id)
+ clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt)
+
+
+async def _update_device(hass, config_entry, config):
+ """Add or update device registry."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ config_entry_id = config_entry.entry_id
+ device_info = {
+ "connections": {(CONNECTION_NETWORK_MAC, config[CONF_MAC])},
+ "manufacturer": config[CONF_MANUFACTURER],
+ "model": config[CONF_MODEL],
+ "name": config[CONF_NAME],
+ "sw_version": config[CONF_SW_VERSION],
+ "config_entry_id": config_entry_id,
+ }
+ _LOGGER.debug("Adding or updating tasmota device %s", config[CONF_MAC])
+ device = device_registry.async_get_or_create(**device_info)
+ hass.data[DEVICE_MACS][device.id] = config[CONF_MAC]
+
+
+async def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt):
+ """Set up the Tasmota device."""
+ if not config:
+ await _remove_device(hass, config_entry, mac, tasmota_mqtt)
+ else:
+ await _update_device(hass, config_entry, config)
diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py
new file mode 100644
index 00000000000000..fbac4bd7dd2e80
--- /dev/null
+++ b/homeassistant/components/tasmota/config_flow.py
@@ -0,0 +1,56 @@
+"""Config flow for Tasmota."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.mqtt import valid_subscribe_topic
+
+from .const import ( # pylint:disable=unused-import
+ CONF_DISCOVERY_PREFIX,
+ DEFAULT_PREFIX,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ return await self.async_step_config()
+
+ async def async_step_config(self, user_input=None):
+ """Confirm the setup."""
+ errors = {}
+ data = {CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX}
+
+ if user_input is not None:
+ bad_prefix = False
+ if self.show_advanced_options:
+ prefix = user_input[CONF_DISCOVERY_PREFIX]
+ try:
+ valid_subscribe_topic(f"{prefix}/#")
+ except vol.Invalid:
+ errors["base"] = "invalid_discovery_topic"
+ bad_prefix = True
+ else:
+ data = user_input
+ if not bad_prefix:
+ return self.async_create_entry(title="Tasmota", data=data)
+
+ fields = {}
+ if self.show_advanced_options:
+ fields[vol.Optional(CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX)] = str
+
+ return self.async_show_form(
+ step_id="config", data_schema=vol.Schema(fields), errors=errors
+ )
diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py
new file mode 100644
index 00000000000000..62504b4a86f2f1
--- /dev/null
+++ b/homeassistant/components/tasmota/const.py
@@ -0,0 +1,6 @@
+"""Constants used by multiple Tasmota modules."""
+CONF_DISCOVERY_PREFIX = "discovery_prefix"
+
+DEFAULT_PREFIX = "tasmota/discovery"
+
+DOMAIN = "tasmota"
diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py
new file mode 100644
index 00000000000000..e6bde93bee8b10
--- /dev/null
+++ b/homeassistant/components/tasmota/discovery.py
@@ -0,0 +1,123 @@
+"""Support for MQTT discovery."""
+import asyncio
+import logging
+
+from hatasmota.discovery import (
+ TasmotaDiscovery,
+ get_device_config as tasmota_get_device_config,
+ get_entities_for_platform as tasmota_get_entities_for_platform,
+ get_entity as tasmota_get_entity,
+ has_entities_with_platform as tasmota_has_entities_with_platform,
+ unique_id_from_hash,
+)
+
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORTED_PLATFORMS = [
+ "switch",
+]
+
+ALREADY_DISCOVERED = "tasmota_discovered_components"
+CONFIG_ENTRY_IS_SETUP = "tasmota_config_entry_is_setup"
+DATA_CONFIG_ENTRY_LOCK = "tasmota_config_entry_lock"
+TASMOTA_DISCOVERY_DEVICE = "tasmota_discovery_device"
+TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}"
+TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}"
+
+
+def clear_discovery_hash(hass, discovery_hash):
+ """Clear entry in ALREADY_DISCOVERED list."""
+ del hass.data[ALREADY_DISCOVERED][discovery_hash]
+
+
+def set_discovery_hash(hass, discovery_hash):
+ """Set entry in ALREADY_DISCOVERED list."""
+ hass.data[ALREADY_DISCOVERED][discovery_hash] = {}
+
+
+async def async_start(
+ hass: HomeAssistantType, discovery_topic, config_entry, tasmota_mqtt
+) -> bool:
+ """Start MQTT Discovery."""
+
+ async def _load_platform(platform):
+ """Load a Tasmota platform if not already done."""
+ async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
+ config_entries_key = f"{platform}.tasmota"
+ if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
+ await hass.config_entries.async_forward_entry_setup(
+ config_entry, platform
+ )
+ hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
+
+ async def _discover_entity(tasmota_entity_config, discovery_hash, platform):
+ """Handle adding or updating a discovered entity."""
+ if not tasmota_entity_config:
+ # Entity disabled, clean up entity registry
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ unique_id = unique_id_from_hash(discovery_hash)
+ entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
+ if entity_id:
+ _LOGGER.debug("Removing entity: %s %s", platform, discovery_hash)
+ entity_registry.async_remove(entity_id)
+ return
+
+ if discovery_hash in hass.data[ALREADY_DISCOVERED]:
+ _LOGGER.debug(
+ "Entity already added, sending update: %s %s",
+ platform,
+ discovery_hash,
+ )
+ async_dispatcher_send(
+ hass,
+ TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
+ tasmota_entity_config,
+ )
+ else:
+ _LOGGER.debug("Adding new entity: %s %s", platform, discovery_hash)
+ tasmota_entity = tasmota_get_entity(tasmota_entity_config, tasmota_mqtt)
+
+ hass.data[ALREADY_DISCOVERED][discovery_hash] = None
+
+ async_dispatcher_send(
+ hass,
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(platform),
+ tasmota_entity,
+ discovery_hash,
+ )
+
+ async def async_device_discovered(payload, mac):
+ """Process the received message."""
+
+ if ALREADY_DISCOVERED not in hass.data:
+ hass.data[ALREADY_DISCOVERED] = {}
+
+ _LOGGER.debug("Received discovery data for tasmota device: %s", mac)
+ tasmota_device_config = tasmota_get_device_config(payload)
+ async_dispatcher_send(
+ hass, TASMOTA_DISCOVERY_DEVICE, tasmota_device_config, mac
+ )
+
+ if not payload:
+ return
+
+ for platform in SUPPORTED_PLATFORMS:
+ if not tasmota_has_entities_with_platform(payload, platform):
+ continue
+ await _load_platform(platform)
+
+ for platform in SUPPORTED_PLATFORMS:
+ tasmota_entities = tasmota_get_entities_for_platform(payload, platform)
+ for (tasmota_entity_config, discovery_hash) in tasmota_entities:
+ await _discover_entity(tasmota_entity_config, discovery_hash, platform)
+
+ hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
+ hass.data[CONFIG_ENTRY_IS_SETUP] = set()
+
+ tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt)
+ await tasmota_discovery.start_discovery(async_device_discovered, None)
diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json
new file mode 100644
index 00000000000000..5540988edcc89e
--- /dev/null
+++ b/homeassistant/components/tasmota/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "tasmota",
+ "name": "Tasmota (beta)",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/tasmota",
+ "requirements": ["hatasmota==0.0.10"],
+ "dependencies": ["mqtt"],
+ "codeowners": ["@emontnemery"]
+}
diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py
new file mode 100644
index 00000000000000..c8099cfbb85e79
--- /dev/null
+++ b/homeassistant/components/tasmota/mixins.py
@@ -0,0 +1,111 @@
+"""Tasnmota entity mixins."""
+import logging
+
+from homeassistant.components.mqtt import (
+ async_subscribe_connection_status,
+ is_connected as mqtt_connected,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+
+from .discovery import (
+ TASMOTA_DISCOVERY_ENTITY_UPDATED,
+ clear_discovery_hash,
+ set_discovery_hash,
+)
+
+DATA_MQTT = "mqtt"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TasmotaEntity(Entity):
+ """Base class for Tasmota entities."""
+
+ def __init__(self, tasmota_entity) -> None:
+ """Initialize."""
+ self._tasmota_entity = tasmota_entity
+
+
+class TasmotaAvailability(TasmotaEntity):
+ """Mixin used for platforms that report availability."""
+
+ def __init__(self, **kwds) -> None:
+ """Initialize the availability mixin."""
+ self._available = False
+ super().__init__(**kwds)
+
+ async def async_added_to_hass(self) -> None:
+ """Subscribe MQTT events."""
+ await super().async_added_to_hass()
+ self._tasmota_entity.set_on_availability_callback(self.availability_updated)
+ self.async_on_remove(
+ async_subscribe_connection_status(self.hass, self.async_mqtt_connected)
+ )
+
+ @callback
+ def availability_updated(self, available: bool) -> None:
+ """Handle updated availability."""
+ self._available = available
+ self.async_write_ha_state()
+
+ @callback
+ def async_mqtt_connected(self, _):
+ """Update state on connection/disconnection to MQTT broker."""
+ if not self.hass.is_stopping:
+ self.async_write_ha_state()
+
+ @property
+ def available(self) -> bool:
+ """Return if the device is available."""
+ if not mqtt_connected(self.hass) and not self.hass.is_stopping:
+ return False
+ return self._available
+
+
+class TasmotaDiscoveryUpdate(TasmotaEntity):
+ """Mixin used to handle updated discovery message."""
+
+ def __init__(self, discovery_hash, discovery_update, **kwds) -> None:
+ """Initialize the discovery update mixin."""
+ self._discovery_hash = discovery_hash
+ self._discovery_update = discovery_update
+ self._removed_from_hass = False
+ super().__init__(**kwds)
+
+ async def async_added_to_hass(self) -> None:
+ """Subscribe to discovery updates."""
+ await super().async_added_to_hass()
+ self._removed_from_hass = False
+
+ async def discovery_callback(config):
+ """Handle discovery update."""
+ _LOGGER.debug(
+ "Got update for entity with hash: %s '%s'",
+ self._discovery_hash,
+ config,
+ )
+ if not self._tasmota_entity.config_same(config):
+ # Changed payload: Notify component
+ _LOGGER.debug("Updating component: %s", self.entity_id)
+ await self._discovery_update(config)
+ else:
+ # Unchanged payload: Ignore to avoid changing states
+ _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id)
+
+ # Set in case the entity has been removed and is re-added, for example when changing entity_id
+ set_discovery_hash(self.hass, self._discovery_hash)
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*self._discovery_hash),
+ discovery_callback,
+ )
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Stop listening to signal and cleanup discovery data.."""
+ if not self._removed_from_hass:
+ clear_discovery_hash(self.hass, self._discovery_hash)
+ self._removed_from_hass = True
diff --git a/homeassistant/components/tasmota/strings.json b/homeassistant/components/tasmota/strings.json
new file mode 100644
index 00000000000000..d19bb09326399a
--- /dev/null
+++ b/homeassistant/components/tasmota/strings.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "step": {
+ "config": {
+ "title": "Tasmota",
+ "description": "Please enter the Tasmota configuration.",
+ "data": {
+ "discovery_prefix": "Discovery topic prefix"
+ }
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
+ "error": {
+ "invalid_discovery_topic": "Invalid discovery topic prefix."
+ }
+ }
+}
diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py
new file mode 100644
index 00000000000000..f087a30958ce8e
--- /dev/null
+++ b/homeassistant/components/tasmota/switch.py
@@ -0,0 +1,116 @@
+"""Support for Tasmota switches."""
+import logging
+
+from homeassistant.components import switch
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DOMAIN as TASMOTA_DOMAIN
+from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
+from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Tasmota switch dynamically through discovery."""
+
+ @callback
+ def async_discover(tasmota_entity, discovery_hash):
+ """Discover and add a Tasmota switch."""
+ async_add_entities(
+ [
+ TasmotaSwitch(
+ tasmota_entity=tasmota_entity, discovery_hash=discovery_hash
+ )
+ ]
+ )
+
+ async_dispatcher_connect(
+ hass,
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN, TASMOTA_DOMAIN),
+ async_discover,
+ )
+
+
+class TasmotaSwitch(
+ TasmotaAvailability,
+ TasmotaDiscoveryUpdate,
+ SwitchEntity,
+):
+ """Representation of a Tasmota switch."""
+
+ def __init__(self, tasmota_entity, **kwds):
+ """Initialize the Tasmota switch."""
+ self._state = False
+ self._sub_state = None
+
+ self._unique_id = tasmota_entity.unique_id
+
+ super().__init__(
+ discovery_update=self.discovery_update,
+ tasmota_entity=tasmota_entity,
+ **kwds,
+ )
+
+ async def async_added_to_hass(self):
+ """Subscribe to MQTT events."""
+ await super().async_added_to_hass()
+ self._tasmota_entity.set_on_state_callback(self.state_updated)
+ await self._subscribe_topics()
+
+ async def discovery_update(self, update):
+ """Handle updated discovery message."""
+ self._tasmota_entity.config_update(update)
+ await self._subscribe_topics()
+ self.async_write_ha_state()
+
+ @callback
+ def state_updated(self, state):
+ """Handle new MQTT state messages."""
+ self._state = state
+ self.async_write_ha_state()
+
+ async def _subscribe_topics(self):
+ """(Re)Subscribe to topics."""
+ await self._tasmota_entity.subscribe_topics()
+
+ async def async_will_remove_from_hass(self):
+ """Unsubscribe when removed."""
+ await self._tasmota_entity.unsubscribe_topics()
+ await super().async_will_remove_from_hass()
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ return self._tasmota_entity.name
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._state
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._tasmota_entity.unique_id
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return {"connections": {(CONNECTION_NETWORK_MAC, self._tasmota_entity.mac)}}
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ self._tasmota_entity.set_state(True)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ self._tasmota_entity.set_state(False)
diff --git a/homeassistant/components/teksavvy/__init__.py b/homeassistant/components/teksavvy/__init__.py
deleted file mode 100644
index ee0dcd1c8102e4..00000000000000
--- a/homeassistant/components/teksavvy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The teksavvy component."""
diff --git a/homeassistant/components/teksavvy/manifest.json b/homeassistant/components/teksavvy/manifest.json
deleted file mode 100644
index e114efdce9f003..00000000000000
--- a/homeassistant/components/teksavvy/manifest.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "domain": "teksavvy",
- "name": "TekSavvy",
- "documentation": "https://www.home-assistant.io/integrations/teksavvy",
- "codeowners": []
-}
diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py
deleted file mode 100644
index 4ff2bc84dbe23d..00000000000000
--- a/homeassistant/components/teksavvy/sensor.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""Support for TekSavvy Bandwidth Monitor."""
-from datetime import timedelta
-import logging
-
-import async_timeout
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_API_KEY,
- CONF_MONITORED_VARIABLES,
- CONF_NAME,
- DATA_GIGABYTES,
- HTTP_OK,
- PERCENTAGE,
-)
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_NAME = "TekSavvy"
-CONF_TOTAL_BANDWIDTH = "total_bandwidth"
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
-REQUEST_TIMEOUT = 5 # seconds
-
-SENSOR_TYPES = {
- "usage": ["Usage Ratio", PERCENTAGE, "mdi:percent"],
- "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"],
- "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"],
- "onpeak_download": ["On Peak Download", DATA_GIGABYTES, "mdi:download"],
- "onpeak_upload": ["On Peak Upload", DATA_GIGABYTES, "mdi:upload"],
- "onpeak_total": ["On Peak Total", DATA_GIGABYTES, "mdi:download"],
- "offpeak_download": ["Off Peak download", DATA_GIGABYTES, "mdi:download"],
- "offpeak_upload": ["Off Peak Upload", DATA_GIGABYTES, "mdi:upload"],
- "offpeak_total": ["Off Peak Total", DATA_GIGABYTES, "mdi:download"],
- "onpeak_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"],
-}
-
-API_HA_MAP = (
- ("OnPeakDownload", "onpeak_download"),
- ("OnPeakUpload", "onpeak_upload"),
- ("OffPeakDownload", "offpeak_download"),
- ("OffPeakUpload", "offpeak_upload"),
-)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_MONITORED_VARIABLES): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_TOTAL_BANDWIDTH): cv.positive_int,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the sensor platform."""
- websession = async_get_clientsession(hass)
- apikey = config.get(CONF_API_KEY)
- bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH)
-
- ts_data = TekSavvyData(hass.loop, websession, apikey, bandwidthcap)
- ret = await ts_data.async_update()
- if ret is False:
- _LOGGER.error("Invalid Teksavvy API key: %s", apikey)
- return
-
- name = config.get(CONF_NAME)
- sensors = []
- for variable in config[CONF_MONITORED_VARIABLES]:
- sensors.append(TekSavvySensor(ts_data, variable, name))
- async_add_entities(sensors, True)
-
-
-class TekSavvySensor(Entity):
- """Representation of TekSavvy Bandwidth sensor."""
-
- def __init__(self, teksavvydata, sensor_type, name):
- """Initialize the sensor."""
- self.client_name = name
- self.type = sensor_type
- self._name = SENSOR_TYPES[sensor_type][0]
- self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
- self._icon = SENSOR_TYPES[sensor_type][2]
- self.teksavvydata = teksavvydata
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self.client_name} {self._name}"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
- """Icon to use in the frontend, if any."""
- return self._icon
-
- async def async_update(self):
- """Get the latest data from TekSavvy and update the state."""
- await self.teksavvydata.async_update()
- if self.type in self.teksavvydata.data:
- self._state = round(self.teksavvydata.data[self.type], 2)
-
-
-class TekSavvyData:
- """Get data from TekSavvy API."""
-
- def __init__(self, loop, websession, api_key, bandwidth_cap):
- """Initialize the data object."""
- self.loop = loop
- self.websession = websession
- self.api_key = api_key
- self.bandwidth_cap = bandwidth_cap
- # Set unlimited users to infinite, otherwise the cap.
- self.data = (
- {"limit": self.bandwidth_cap}
- if self.bandwidth_cap > 0
- else {"limit": float("inf")}
- )
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- async def async_update(self):
- """Get the TekSavvy bandwidth data from the web service."""
- headers = {"TekSavvy-APIKey": self.api_key}
- _LOGGER.debug("Updating TekSavvy data")
- url = (
- "https://api.teksavvy.com/"
- "web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true"
- )
- with async_timeout.timeout(REQUEST_TIMEOUT):
- req = await self.websession.get(url, headers=headers)
- if req.status != HTTP_OK:
- _LOGGER.error("Request failed with status: %u", req.status)
- return False
-
- try:
- data = await req.json()
- for (api, ha_name) in API_HA_MAP:
- self.data[ha_name] = float(data["value"][0][api])
- on_peak_download = self.data["onpeak_download"]
- on_peak_upload = self.data["onpeak_upload"]
- off_peak_download = self.data["offpeak_download"]
- off_peak_upload = self.data["offpeak_upload"]
- limit = self.data["limit"]
- # Support "unlimited" users
- if self.bandwidth_cap > 0:
- self.data["usage"] = 100 * on_peak_download / self.bandwidth_cap
- else:
- self.data["usage"] = 0
- self.data["usage_gb"] = on_peak_download
- self.data["onpeak_total"] = on_peak_download + on_peak_upload
- self.data["offpeak_total"] = off_peak_download + off_peak_upload
- self.data["onpeak_remaining"] = limit - on_peak_download
- return True
- except ValueError:
- _LOGGER.error("JSON Decode Failed")
- return False
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index 8e42b25c0caf9d..a87bfdf3af6b9f 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -202,7 +202,7 @@ send_location:
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_message:
- description: Edit a previusly sent message.
+ description: Edit a previously sent message.
fields:
message_id:
description: id of the message to edit.
@@ -227,7 +227,7 @@ edit_message:
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_caption:
- description: Edit the caption of a previusly sent message.
+ description: Edit the caption of a previously sent message.
fields:
message_id:
description: id of the message to edit.
@@ -243,7 +243,7 @@ edit_caption:
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_replymarkup:
- description: Edit the inline keyboard of a previusly sent message.
+ description: Edit the inline keyboard of a previously sent message.
fields:
message_id:
description: id of the message to edit.
diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index c8f27a9412a406..e322481813a82c 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -6,6 +6,8 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
SPEED_METERS_PER_SECOND,
@@ -40,14 +42,19 @@
DEVICE_CLASS_TEMPERATURE,
],
SENSOR_TYPE_HUMIDITY: ["Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY],
- SENSOR_TYPE_RAINRATE: ["Rain rate", f"mm/{TIME_HOURS}", "mdi:water", None],
- SENSOR_TYPE_RAINTOTAL: ["Rain total", "mm", "mdi:water", None],
+ SENSOR_TYPE_RAINRATE: [
+ "Rain rate",
+ f"{LENGTH_MILLIMETERS}/{TIME_HOURS}",
+ "mdi:water",
+ None,
+ ],
+ SENSOR_TYPE_RAINTOTAL: ["Rain total", LENGTH_MILLIMETERS, "mdi:water", None],
SENSOR_TYPE_WINDDIRECTION: ["Wind direction", "", "", None],
SENSOR_TYPE_WINDAVERAGE: ["Wind average", SPEED_METERS_PER_SECOND, "", None],
SENSOR_TYPE_WINDGUST: ["Wind gust", SPEED_METERS_PER_SECOND, "", None],
SENSOR_TYPE_UV: ["UV", UV_INDEX, "", None],
SENSOR_TYPE_WATT: ["Power", POWER_WATT, "", None],
- SENSOR_TYPE_LUMINANCE: ["Luminance", "lx", None, DEVICE_CLASS_ILLUMINANCE],
+ SENSOR_TYPE_LUMINANCE: ["Luminance", LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE],
SENSOR_TYPE_DEW_POINT: ["Dew Point", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
SENSOR_TYPE_BAROMETRIC_PRESSURE: ["Barometric Pressure", "kPa", "", None],
}
diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json
index 8145717b40e973..13054967365281 100644
--- a/homeassistant/components/tellduslive/translations/pl.json
+++ b/homeassistant/components/tellduslive/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "TelldusLive jest ju\u017c skonfigurowany",
"authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
"auth_error": "B\u0142\u0105d uwierzytelniania, spr\u00f3buj ponownie"
diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py
index dc9e5ead1d03d1..fe8b6568c1e3e7 100644
--- a/homeassistant/components/template/cover.py
+++ b/homeassistant/components/template/cover.py
@@ -29,7 +29,9 @@
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_CLOSED,
+ STATE_CLOSING,
STATE_OPEN,
+ STATE_OPENING,
)
from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
@@ -42,7 +44,14 @@
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
-_VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"]
+_VALID_STATES = [
+ STATE_OPEN,
+ STATE_OPENING,
+ STATE_CLOSED,
+ STATE_CLOSING,
+ "true",
+ "false",
+]
CONF_COVERS = "covers"
diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py
index 2e42b28fbd2188..49b0edfab0230f 100644
--- a/homeassistant/components/template/template_entity.py
+++ b/homeassistant/components/template/template_entity.py
@@ -5,6 +5,7 @@
import voluptuous as vol
+from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
@@ -121,7 +122,6 @@ def __init__(
"""Template Entity."""
self._template_attrs = {}
self._async_update = None
- self._async_update_entity_ids_filter = None
self._attribute_templates = attribute_templates
self._attributes = {}
self._availability_template = availability_template
@@ -130,6 +130,7 @@ def __init__(
self._entity_picture_template = entity_picture_template
self._icon = None
self._entity_picture = None
+ self._self_ref_update_count = 0
@property
def should_poll(self):
@@ -212,7 +213,6 @@ def add_template_attribute(
attribute = _TemplateAttribute(
self, attribute, template, validator, on_update, none_on_template_error
)
- attribute.async_setup()
self._template_attrs.setdefault(template, [])
self._template_attrs[template].append(attribute)
@@ -223,38 +223,47 @@ def _handle_results(
updates: List[TrackTemplateResult],
) -> None:
"""Call back the results to the attributes."""
+
if event:
self.async_set_context(event.context)
+ entity_id = event and event.data.get(ATTR_ENTITY_ID)
+
+ if entity_id and entity_id == self.entity_id:
+ self._self_ref_update_count += 1
+ else:
+ self._self_ref_update_count = 0
+
+ if self._self_ref_update_count > len(self._template_attrs):
+ for update in updates:
+ _LOGGER.warning(
+ "Template loop detected while processing event: %s, skipping template render for Template[%s]",
+ event,
+ update.template.template,
+ )
+ return
+
for update in updates:
for attr in self._template_attrs[update.template]:
attr.handle_result(
event, update.template, update.last_result, update.result
)
- if self._async_update_entity_ids_filter:
- self._async_update_entity_ids_filter({self.entity_id})
-
- if self._async_update:
- self.async_write_ha_state()
+ self.async_write_ha_state()
async def _async_template_startup(self, *_) -> None:
- # _handle_results will not write state until "_async_update" is set
- template_var_tups = [
- TrackTemplate(template, None) for template in self._template_attrs
- ]
+ template_var_tups = []
+ for template, attributes in self._template_attrs.items():
+ template_var_tups.append(TrackTemplate(template, None))
+ for attribute in attributes:
+ attribute.async_setup()
result_info = async_track_template_result(
self.hass, template_var_tups, self._handle_results
)
self.async_on_remove(result_info.async_remove)
- result_info.async_refresh()
- result_info.async_update_entity_ids_filter({self.entity_id})
- self.async_write_ha_state()
self._async_update = result_info.async_refresh
- self._async_update_entity_ids_filter = (
- result_info.async_update_entity_ids_filter
- )
+ result_info.async_refresh()
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py
index 9e46a30972f5b7..debe896c9cf682 100644
--- a/homeassistant/components/tesla/config_flow.py
+++ b/homeassistant/components/tesla/config_flow.py
@@ -11,6 +11,7 @@
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
)
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
@@ -61,7 +62,7 @@ async def async_step_user(self, user_input=None):
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
- errors={CONF_USERNAME: "identifier_exists"},
+ errors={CONF_USERNAME: "already_configured"},
description_placeholders={},
)
@@ -71,14 +72,14 @@ async def async_step_user(self, user_input=None):
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
- errors={"base": "connection_error"},
+ errors={"base": "cannot_connect"},
description_placeholders={},
)
except InvalidAuth:
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
- errors={"base": "invalid_credentials"},
+ errors={"base": "invalid_auth"},
description_placeholders={},
)
return self.async_create_entry(title=user_input[CONF_USERNAME], data=info)
@@ -140,7 +141,7 @@ async def validate_input(hass: core.HomeAssistant, data):
test_login=True
)
except TeslaException as ex:
- if ex.code == 401:
+ if ex.code == HTTP_UNAUTHORIZED:
_LOGGER.error("Invalid credentials: %s", ex)
raise InvalidAuth() from ex
_LOGGER.error("Unable to communicate with Tesla API: %s", ex)
diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json
index fb3c11a276fdd5..503124eedd4e8f 100644
--- a/homeassistant/components/tesla/strings.json
+++ b/homeassistant/components/tesla/strings.json
@@ -1,10 +1,9 @@
{
"config": {
"error": {
- "connection_error": "Error connecting; check network and retry",
- "identifier_exists": "Email already registered",
- "invalid_credentials": "Invalid credentials",
- "unknown_error": "Unknown error, please report log info"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"step": {
"user": {
@@ -27,4 +26,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/tesla/translations/ca.json b/homeassistant/components/tesla/translations/ca.json
index 35169f70245c4d..0d39ebc26392f6 100644
--- a/homeassistant/components/tesla/translations/ca.json
+++ b/homeassistant/components/tesla/translations/ca.json
@@ -1,8 +1,12 @@
{
"config": {
"error": {
+ "already_configured": "El compte ja ha estat configurat",
+ "already_configured_account": "El compte ja ha estat configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "Error de connexi\u00f3; comprova la xarxa i torna-ho a intentar",
"identifier_exists": "Correu electr\u00f2nic ja registrat",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"invalid_credentials": "Credencials inv\u00e0lides",
"unknown_error": "Error desconegut, si us plau, envia la informaci\u00f3 del registre"
},
diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json
index 762a7f01d0ecbf..80d5591b0a129e 100644
--- a/homeassistant/components/tesla/translations/en.json
+++ b/homeassistant/components/tesla/translations/en.json
@@ -1,8 +1,12 @@
{
"config": {
"error": {
+ "already_configured": "Account is already configured",
+ "already_configured_account": "Account is already configured",
+ "cannot_connect": "Failed to connect",
"connection_error": "Error connecting; check network and retry",
"identifier_exists": "Email already registered",
+ "invalid_auth": "Invalid authentication",
"invalid_credentials": "Invalid credentials",
"unknown_error": "Unknown error, please report log info"
},
diff --git a/homeassistant/components/tesla/translations/es.json b/homeassistant/components/tesla/translations/es.json
index 8bb659d377fe22..08518205257cc4 100644
--- a/homeassistant/components/tesla/translations/es.json
+++ b/homeassistant/components/tesla/translations/es.json
@@ -1,8 +1,11 @@
{
"config": {
"error": {
+ "already_configured_account": "La cuenta ya ha sido configurada",
+ "cannot_connect": "No se pudo conectar",
"connection_error": "Error de conexi\u00f3n; comprueba la red y vuelve a intentarlo",
"identifier_exists": "Correo electr\u00f3nico ya registrado",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"invalid_credentials": "Credenciales no v\u00e1lidas",
"unknown_error": "Error desconocido, por favor reporte la informaci\u00f3n de registro"
},
diff --git a/homeassistant/components/tesla/translations/et.json b/homeassistant/components/tesla/translations/et.json
new file mode 100644
index 00000000000000..baf6178bb262fd
--- /dev/null
+++ b/homeassistant/components/tesla/translations/et.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Konto on juba h\u00e4\u00e4lestatud",
+ "already_configured_account": "Kasutaja on juba lisatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json
index 0c4ae68fe0ffd4..5a14b01cc88aec 100644
--- a/homeassistant/components/tesla/translations/it.json
+++ b/homeassistant/components/tesla/translations/it.json
@@ -1,8 +1,12 @@
{
"config": {
"error": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "already_configured_account": "L'account \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Errore durante la connessione; controllare la rete e riprovare",
"identifier_exists": "E-mail gi\u00e0 registrata",
+ "invalid_auth": "Autenticazione non valida",
"invalid_credentials": "Credenziali non valide",
"unknown_error": "Errore sconosciuto, si prega di segnalare le informazioni del registro"
},
diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json
index a2ca81bf693ed0..6c9e047ff629fc 100644
--- a/homeassistant/components/tesla/translations/no.json
+++ b/homeassistant/components/tesla/translations/no.json
@@ -1,8 +1,11 @@
{
"config": {
"error": {
+ "already_configured_account": "Kontoen er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Feil ved tilkobling; sjekk nettverket og pr\u00f8v p\u00e5 nytt",
"identifier_exists": "E-post er allerede registrert",
+ "invalid_auth": "Ugyldig godkjenning",
"invalid_credentials": "Ugyldig legitimasjon",
"unknown_error": "Ukjent feil, Vennligst rapporter informasjon fra Loggen"
},
diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json
index e2a6b6a09c82a9..99623c0d7732af 100644
--- a/homeassistant/components/tesla/translations/pl.json
+++ b/homeassistant/components/tesla/translations/pl.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie",
"identifier_exists": "Adres e-mail jest ju\u017c zarejestrowany.",
"invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia",
diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json
index dae30a669b32d9..10a1239279c75e 100644
--- a/homeassistant/components/tesla/translations/ru.json
+++ b/homeassistant/components/tesla/translations/ru.json
@@ -1,8 +1,12 @@
{
"config": {
"error": {
+ "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_account": "\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.",
+ "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.",
"identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py
index 35a16d30f32024..3555816213a7b8 100644
--- a/homeassistant/components/thethingsnetwork/sensor.py
+++ b/homeassistant/components/thethingsnetwork/sensor.py
@@ -8,7 +8,7 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND
+from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -135,7 +135,7 @@ async def async_update(self):
_LOGGER.error("The device is not available: %s", self._device_id)
return None
- if status == 401:
+ if status == HTTP_UNAUTHORIZED:
_LOGGER.error("Not authorized for Application ID: %s", self._app_id)
return None
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index b3decdd250a3cf..74f61f17d29404 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
- "requirements": ["pyTibber==0.15.2"],
+ "requirements": ["pyTibber==0.15.3"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index 939c6d1597de59..c5484f908386fd 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -2,6 +2,7 @@
import asyncio
from datetime import timedelta
import logging
+from random import randrange
import aiohttp
@@ -59,6 +60,7 @@ def __init__(self, tibber_home):
self._name = tibber_home.info["viewer"]["home"]["address"].get(
"address1", ""
)
+ self._spread_load_constant = randrange(3600)
@property
def device_state_attributes(self):
@@ -110,7 +112,8 @@ async def async_update(self):
if (
not self._tibber_home.last_data_timestamp
- or (self._tibber_home.last_data_timestamp - now).total_seconds() / 3600 < 12
+ or (self._tibber_home.last_data_timestamp - now).total_seconds()
+ < 12 * 3600 + self._spread_load_constant
or not self._is_available
):
_LOGGER.debug("Asking for new data")
@@ -156,9 +159,9 @@ def unique_id(self):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self):
+ _LOGGER.debug("Fetching data")
try:
- await self._tibber_home.update_info()
- await self._tibber_home.update_price_info()
+ await self._tibber_home.update_info_and_price_info()
except (asyncio.TimeoutError, aiohttp.ClientError):
return
data = self._tibber_home.info["viewer"]["home"]
diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json
index cb5cc08552aded..25af2c7f30d348 100644
--- a/homeassistant/components/tibber/strings.json
+++ b/homeassistant/components/tibber/strings.json
@@ -2,11 +2,11 @@
"title": "Tibber",
"config": {
"abort": {
- "already_configured": "A Tibber account is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"timeout": "Timeout connecting to Tibber",
- "connection_error": "Error connecting to Tibber",
+ "connection_error": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]"
},
"step": {
@@ -19,4 +19,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/tibber/translations/ca.json b/homeassistant/components/tibber/translations/ca.json
index 0b17cbb352459e..111c9e568f3102 100644
--- a/homeassistant/components/tibber/translations/ca.json
+++ b/homeassistant/components/tibber/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Ja hi ha un compte Tibber configurat."
+ "already_configured": "El servei ja est\u00e0 configurat"
},
"error": {
- "connection_error": "S'ha produ\u00eft un error en connectar-se a Tibber",
+ "connection_error": "Ha fallat la connexi\u00f3",
"invalid_access_token": "[%key::common::config_flow::error::invalid_access_token%]",
"timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 a Tibber"
},
diff --git a/homeassistant/components/tibber/translations/en.json b/homeassistant/components/tibber/translations/en.json
index 658ce0049433e1..85d720a8c66d28 100644
--- a/homeassistant/components/tibber/translations/en.json
+++ b/homeassistant/components/tibber/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "A Tibber account is already configured."
+ "already_configured": "Service is already configured"
},
"error": {
- "connection_error": "Error connecting to Tibber",
+ "connection_error": "Failed to connect",
"invalid_access_token": "Invalid access token",
"timeout": "Timeout connecting to Tibber"
},
diff --git a/homeassistant/components/tibber/translations/it.json b/homeassistant/components/tibber/translations/it.json
index 3a5548360cf7cb..fcaf76721fdb8a 100644
--- a/homeassistant/components/tibber/translations/it.json
+++ b/homeassistant/components/tibber/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Un account Tibber \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
"error": {
- "connection_error": "Errore durante la connessione a Tibber",
+ "connection_error": "Impossibile connettersi",
"invalid_access_token": "Token di accesso non valido",
"timeout": "Tempo scaduto per la connessione a Tibber"
},
diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json
index 34e078f54677ab..bdfaa580365492 100644
--- a/homeassistant/components/tibber/translations/no.json
+++ b/homeassistant/components/tibber/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "En Tibber-konto er allerede konfigurert."
+ "already_configured": "Tjenesten er allerede konfigurert"
},
"error": {
- "connection_error": "Feil ved tilkobling til Tibber",
+ "connection_error": "Tilkobling mislyktes.",
"invalid_access_token": "Ugyldig tilgangstoken",
"timeout": "Tidsavbrudd for tilkobling til Tibber"
},
diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json
index 8ef963583015bb..d69572b4a42b07 100644
--- a/homeassistant/components/tibber/translations/pl.json
+++ b/homeassistant/components/tibber/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane."
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"connection_error": "B\u0142\u0105d po\u0142\u0105czenia z Tibber.",
- "invalid_access_token": "Niepoprawny token dost\u0119pu.",
+ "invalid_access_token": "Niepoprawny token dost\u0119pu",
"timeout": "Przekroczono limit czasu \u0142\u0105czenia z Tibber."
},
"step": {
diff --git a/homeassistant/components/tibber/translations/ru.json b/homeassistant/components/tibber/translations/ru.json
index 715fcf1179ae75..294c0c75ac80b2 100644
--- a/homeassistant/components/tibber/translations/ru.json
+++ b/homeassistant/components/tibber/translations/ru.json
@@ -1,10 +1,10 @@
{
"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 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
- "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\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_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.",
"timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f."
},
diff --git a/homeassistant/components/tibber/translations/zh-Hant.json b/homeassistant/components/tibber/translations/zh-Hant.json
index 0521ff792664a4..470def61daf6f0 100644
--- a/homeassistant/components/tibber/translations/zh-Hant.json
+++ b/homeassistant/components/tibber/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Tibber \u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "connection_error": "\u9023\u7dda\u81f3 Tibber \u932f\u8aa4",
+ "connection_error": "\u9023\u7dda\u5931\u6557",
"invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548",
"timeout": "\u9023\u7dda\u81f3 Tibber \u903e\u6642"
},
diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py
index 15ac70eeb2c3e7..87f58193e9d595 100644
--- a/homeassistant/components/tile/config_flow.py
+++ b/homeassistant/components/tile/config_flow.py
@@ -47,6 +47,6 @@ async def async_step_user(self, user_input=None):
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session
)
except TileError:
- return await self._show_form({"base": "invalid_credentials"})
+ return await self._show_form({"base": "invalid_auth"})
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
diff --git a/homeassistant/components/tile/strings.json b/homeassistant/components/tile/strings.json
index 8a1ee9660d918c..cdddbe54f96366 100644
--- a/homeassistant/components/tile/strings.json
+++ b/homeassistant/components/tile/strings.json
@@ -10,10 +10,10 @@
}
},
"error": {
- "invalid_credentials": "Invalid Tile credentials provided."
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "This Tile account is already registered."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
},
"options": {
diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json
index b76312d957f741..dfc968eb066552 100644
--- a/homeassistant/components/tile/translations/de.json
+++ b/homeassistant/components/tile/translations/de.json
@@ -5,7 +5,18 @@
"data": {
"password": "Passwort",
"username": "E-Mail Adresse"
- }
+ },
+ "title": "Kachel konfigurieren"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_inactive": "Inaktive Kacheln anzeigen"
+ },
+ "title": "Kachel konfigurieren"
}
}
}
diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py
index 548cac6da92066..f81a4a7249a8fa 100644
--- a/homeassistant/components/todoist/calendar.py
+++ b/homeassistant/components/todoist/calendar.py
@@ -262,14 +262,13 @@ def device_state_attributes(self):
# No tasks, we don't REALLY need to show anything.
return None
- attributes = {}
- attributes[DUE_TODAY] = self.data.event[DUE_TODAY]
- attributes[OVERDUE] = self.data.event[OVERDUE]
- attributes[ALL_TASKS] = self._cal_data[ALL_TASKS]
- attributes[PRIORITY] = self.data.event[PRIORITY]
- attributes[LABELS] = self.data.event[LABELS]
-
- return attributes
+ return {
+ DUE_TODAY: self.data.event[DUE_TODAY],
+ OVERDUE: self.data.event[OVERDUE],
+ ALL_TASKS: self._cal_data[ALL_TASKS],
+ PRIORITY: self.data.event[PRIORITY],
+ LABELS: self.data.event[LABELS],
+ }
class TodoistProjectData:
diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py
index 58f50f4899ecfc..d9ad178cab284b 100644
--- a/homeassistant/components/tof/sensor.py
+++ b/homeassistant/components/tof/sensor.py
@@ -9,14 +9,12 @@
from homeassistant.components import rpi_gpio
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-LENGTH_MILLIMETERS = "mm"
-
CONF_I2C_ADDRESS = "i2c_address"
CONF_I2C_BUS = "i2c_bus"
CONF_XSHUT = "xshut"
diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py
index 873da5a78640e8..ce660a60280502 100644
--- a/homeassistant/components/tomato/device_tracker.py
+++ b/homeassistant/components/tomato/device_tracker.py
@@ -19,6 +19,7 @@
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
)
import homeassistant.helpers.config_validation as cv
@@ -111,7 +112,7 @@ def _update_tomato_info(self):
self.last_results[param] = json.loads(value.replace("'", '"'))
return True
- if response.status_code == 401:
+ if response.status_code == HTTP_UNAUTHORIZED:
# Authentication error
_LOGGER.exception(
"Failed to authenticate, please check your username and password"
diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json
index 89c8c2425160a5..7fc0aef9ec6ace 100644
--- a/homeassistant/components/toon/translations/ca.json
+++ b/homeassistant/components/toon/translations/ca.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "L'acord seleccionat ja est\u00e0 configurat.",
"authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.",
- "authorize_url_timeout": "Temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3 esgotat.",
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
"no_agreements": "Aquest compte no t\u00e9 pantalles Toon.",
"no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})"
diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json
index 28e8a1dcb61646..b6c6e7ad67d89b 100644
--- a/homeassistant/components/toon/translations/es.json
+++ b/homeassistant/components/toon/translations/es.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Error desconocido generando una url de autorizaci\u00f3n",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.",
- "no_agreements": "Esta cuenta no tiene pantallas Toon."
+ "no_agreements": "Esta cuenta no tiene pantallas Toon.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json
index c3384f56319967..caeed852d0a0e9 100644
--- a/homeassistant/components/toon/translations/fr.json
+++ b/homeassistant/components/toon/translations/fr.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.",
"authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.",
"missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.",
- "no_agreements": "Ce compte n'a pas d'affichages Toon."
+ "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} )"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json
index bebd8bb912e472..379058f68d1109 100644
--- a/homeassistant/components/toon/translations/ko.json
+++ b/homeassistant/components/toon/translations/ko.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json
index 5d7095d0c85ee5..6491c66673863c 100644
--- a/homeassistant/components/toon/translations/lb.json
+++ b/homeassistant/components/toon/translations/lb.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.",
"authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.",
"missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.",
- "no_agreements": "D\u00ebse Kont huet keen Toon Ecran."
+ "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json
index 69eabaaf28bada..da3ce6d84c7acd 100644
--- a/homeassistant/components/toon/translations/nl.json
+++ b/homeassistant/components/toon/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "no_agreements": "Dit account heeft geen Toon schermen."
+ "no_agreements": "Dit account heeft geen Toon schermen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json
index 37652c4aee18a1..49103a77b3753c 100644
--- a/homeassistant/components/toon/translations/no.json
+++ b/homeassistant/components/toon/translations/no.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.",
"authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
"missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
- "no_agreements": "Denne kontoen har ingen Toon skjermer."
+ "no_agreements": "Denne kontoen har ingen Toon skjermer.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json
index a6d5227afc65e4..020938792d63ee 100644
--- a/homeassistant/components/toon/translations/zh-Hant.json
+++ b/homeassistant/components/toon/translations/zh-Hant.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4",
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
- "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002"
+ "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py
index 03ddd0a432a145..3bc15a441d13a5 100644
--- a/homeassistant/components/totalconnect/config_flow.py
+++ b/homeassistant/components/totalconnect/config_flow.py
@@ -38,7 +38,7 @@ async def async_step_user(self, user_input=None):
data={CONF_USERNAME: username, CONF_PASSWORD: password},
)
# authentication failed / invalid
- errors["base"] = "login"
+ errors["base"] = "invalid_auth"
data_schema = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json
index eb7a91ef4389ed..7b306554b7be6f 100644
--- a/homeassistant/components/totalconnect/strings.json
+++ b/homeassistant/components/totalconnect/strings.json
@@ -10,10 +10,10 @@
}
},
"error": {
- "login": "Login error: please check your username & password"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_configured": "Account already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json
index ca9e6d66e2ea74..f217eb4f6bcf07 100644
--- a/homeassistant/components/totalconnect/translations/ca.json
+++ b/homeassistant/components/totalconnect/translations/ca.json
@@ -4,6 +4,7 @@
"already_configured": "El compte ja ha estat configurat"
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"login": "Error d'inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/el.json b/homeassistant/components/totalconnect/translations/el.json
new file mode 100644
index 00000000000000..161d646f430ec8
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json
index 3486d42ae3d92d..1174d3e668753a 100644
--- a/homeassistant/components/totalconnect/translations/en.json
+++ b/homeassistant/components/totalconnect/translations/en.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Account already configured"
+ "already_configured": "Account is already configured"
},
"error": {
+ "invalid_auth": "Invalid authentication",
"login": "Login error: please check your username & password"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json
index 18b45f390b2b11..d308b2c7170d7d 100644
--- a/homeassistant/components/totalconnect/translations/es.json
+++ b/homeassistant/components/totalconnect/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "La cuenta ya est\u00e1 configurada"
},
"error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"login": "Error de inicio de sesi\u00f3n: comprueba tu nombre de usuario y contrase\u00f1a"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json
new file mode 100644
index 00000000000000..2227b7442a79c6
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Tuvastamise viga"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json
index ef7cf7f38fac23..cf841328d9cabb 100644
--- a/homeassistant/components/totalconnect/translations/fr.json
+++ b/homeassistant/components/totalconnect/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "Compte d\u00e9j\u00e0 configur\u00e9"
},
"error": {
+ "invalid_auth": "Authentification invalide",
"login": "Erreur de connexion: veuillez v\u00e9rifier votre nom d'utilisateur et votre mot de passe"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json
index 455b8a7967feb7..369b739e84146c 100644
--- a/homeassistant/components/totalconnect/translations/it.json
+++ b/homeassistant/components/totalconnect/translations/it.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Account gi\u00e0 configurato"
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
+ "invalid_auth": "Autenticazione non valida",
"login": "Errore di accesso: si prega di controllare il nome utente e la password"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json
index c312f98f3d2a8d..62e4fffb1fdc06 100644
--- a/homeassistant/components/totalconnect/translations/no.json
+++ b/homeassistant/components/totalconnect/translations/no.json
@@ -4,6 +4,7 @@
"already_configured": "Kontoen er allerede konfigurert"
},
"error": {
+ "invalid_auth": "Ugyldig godkjenning",
"login": "P\u00e5loggingsfeil: Vennligst sjekk brukernavnet ditt og passordet ditt"
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json
index a3de1ee4555ef7..29a070d841ed39 100644
--- a/homeassistant/components/totalconnect/translations/ru.json
+++ b/homeassistant/components/totalconnect/translations/ru.json
@@ -4,6 +4,7 @@
"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": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json
index 0424db3cdc3cec..b9416b0fbf343e 100644
--- a/homeassistant/components/totalconnect/translations/zh-Hant.json
+++ b/homeassistant/components/totalconnect/translations/zh-Hant.json
@@ -4,6 +4,7 @@
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u5bc6\u78bc"
},
"step": {
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
index 7ecced32341d0c..6c9d1f8b2e8a9f 100644
--- a/homeassistant/components/tplink/common.py
+++ b/homeassistant/components/tplink/common.py
@@ -119,7 +119,14 @@ def get_static_devices(config_data) -> SmartDevices:
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
elif type_ == CONF_STRIP:
- for plug in SmartStrip(host).plugs.values():
+ try:
+ ss_host = SmartStrip(host)
+ except SmartDeviceException as sde:
+ _LOGGER.error(
+ "Failed to setup SmartStrip at %s: %s; not retrying", host, sde
+ )
+ continue
+ for plug in ss_host.plugs.values():
switches.append(plug)
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json
index cbb896536072a9..a10c44b925262c 100644
--- a/homeassistant/components/tplink/strings.json
+++ b/homeassistant/components/tplink/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration is necessary.",
- "no_devices_found": "No TP-Link devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/tplink/translations/ca.json b/homeassistant/components/tplink/translations/ca.json
index 62baf289fd434a..69dfc1b4b9d6c1 100644
--- a/homeassistant/components/tplink/translations/ca.json
+++ b/homeassistant/components/tplink/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius TP-Link a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/en.json b/homeassistant/components/tplink/translations/en.json
index 3c1f7da52e0cbe..1105f6a383bf27 100644
--- a/homeassistant/components/tplink/translations/en.json
+++ b/homeassistant/components/tplink/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No TP-Link devices found on the network.",
- "single_instance_allowed": "Only a single configuration is necessary."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/it.json b/homeassistant/components/tplink/translations/it.json
index d5fc0a46a00ebb..8940b1c8ee6180 100644
--- a/homeassistant/components/tplink/translations/it.json
+++ b/homeassistant/components/tplink/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo TP-Link trovato in rete.",
- "single_instance_allowed": "\u00c8 necessaria una sola configurazione."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/no.json b/homeassistant/components/tplink/translations/no.json
index d480a5d996dbe9..1d1d624ab40ecf 100644
--- a/homeassistant/components/tplink/translations/no.json
+++ b/homeassistant/components/tplink/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Kun en konfigurasjon av TP-Link er n\u00f8dvendig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/ru.json b/homeassistant/components/tplink/translations/ru.json
index 7f84067d9a2821..4df755bee4f09c 100644
--- a/homeassistant/components/tplink/translations/ru.json
+++ b/homeassistant/components/tplink/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 TP-Link \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json
index 9fafcbbce7d676..e88d982b8a1198 100644
--- a/homeassistant/components/tplink/translations/zh-Hant.json
+++ b/homeassistant/components/tplink/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 TP-Link \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21\u5373\u53ef\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json
index 8574f4f34f11fe..aef269defcb3af 100644
--- a/homeassistant/components/traccar/strings.json
+++ b/homeassistant/components/traccar/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar."
},
"create_entry": {
diff --git a/homeassistant/components/traccar/translations/ca.json b/homeassistant/components/traccar/translations/ca.json
index 2fa9369055391e..8dd1cecf90a7ce 100644
--- a/homeassistant/components/traccar/translations/ca.json
+++ b/homeassistant/components/traccar/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Traccar.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent enlla\u00e7: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
diff --git a/homeassistant/components/traccar/translations/el.json b/homeassistant/components/traccar/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/traccar/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/en.json b/homeassistant/components/traccar/translations/en.json
index 1e6b286def2f58..2a9c92cc1d0386 100644
--- a/homeassistant/components/traccar/translations/en.json
+++ b/homeassistant/components/traccar/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json
index b9cd4b9def9649..11933eafe12d7c 100644
--- a/homeassistant/components/traccar/translations/es.json
+++ b/homeassistant/components/traccar/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
diff --git a/homeassistant/components/traccar/translations/et.json b/homeassistant/components/traccar/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/traccar/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/fr.json b/homeassistant/components/traccar/translations/fr.json
index faf64359f0d1ab..e000bbaaac8f5f 100644
--- a/homeassistant/components/traccar/translations/fr.json
+++ b/homeassistant/components/traccar/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Traccar.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Traccar. \n\n Utilisez l'URL suivante: ` {webhook_url} ` \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json
index 54e46e8d31d240..86e65ace3db3ee 100644
--- a/homeassistant/components/traccar/translations/it.json
+++ b/homeassistant/components/traccar/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Traccar.",
- "one_instance_allowed": "\u00c8 necessaria solo una singola istanza."
+ "one_instance_allowed": "\u00c8 necessaria solo una singola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ."
diff --git a/homeassistant/components/traccar/translations/lb.json b/homeassistant/components/traccar/translations/lb.json
index a4d1da866e03e2..3ccacf127e9ade 100644
--- a/homeassistant/components/traccar/translations/lb.json
+++ b/homeassistant/components/traccar/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Traccar Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen."
diff --git a/homeassistant/components/traccar/translations/no.json b/homeassistant/components/traccar/translations/no.json
index 53e6500e70e541..b471cbb2187bdc 100644
--- a/homeassistant/components/traccar/translations/no.json
+++ b/homeassistant/components/traccar/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: `{webhook_url}`\n\nSe [dokumentasjonen]({docs_url}) for mer informasjon."
diff --git a/homeassistant/components/traccar/translations/ru.json b/homeassistant/components/traccar/translations/ru.json
index a2979379e18a76..f9a23647422dd5 100644
--- a/homeassistant/components/traccar/translations/ru.json
+++ b/homeassistant/components/traccar/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Traccar.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\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]({docs_url}) \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."
diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json
index 5135320f631cbb..b14b5f12b9fdb7 100644
--- a/homeassistant/components/traccar/translations/zh-Hant.json
+++ b/homeassistant/components/traccar/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py
index 2d99de7756ac75..72597637bd3e0b 100644
--- a/homeassistant/components/tradfri/cover.py
+++ b/homeassistant/components/tradfri/cover.py
@@ -31,8 +31,7 @@ def __init__(self, device, api, gateway_id):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attr = {ATTR_MODEL: self._device.device_info.model_number}
- return attr
+ return {ATTR_MODEL: self._device.device_info.model_number}
@property
def current_cover_position(self):
diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json
index 33a3f059098f39..45850fd639a0eb 100644
--- a/homeassistant/components/tradfri/strings.json
+++ b/homeassistant/components/tradfri/strings.json
@@ -12,12 +12,12 @@
},
"error": {
"invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.",
- "cannot_connect": "Unable to connect to the gateway.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout": "Timeout validating the code."
},
"abort": {
- "already_configured": "Bridge is already configured.",
- "already_in_progress": "Bridge configuration is already in progress."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/tradfri/translations/ca.json b/homeassistant/components/tradfri/translations/ca.json
index cee54ccca79eae..a1ab94a09204c9 100644
--- a/homeassistant/components/tradfri/translations/ca.json
+++ b/homeassistant/components/tradfri/translations/ca.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
- "already_in_progress": "La configuraci\u00f3 de l'enlla\u00e7 ja est\u00e0 en curs."
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar a la passarel\u00b7la d'enlla\u00e7",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_key": "Ha fallat el registre amb la clau proporcionada. Si aix\u00f2 continua passant, intenta reiniciar la passarel\u00b7la d'enlla\u00e7.",
"timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi."
},
diff --git a/homeassistant/components/tradfri/translations/en.json b/homeassistant/components/tradfri/translations/en.json
index f1ee9b9238a865..9035496a0cc4f8 100644
--- a/homeassistant/components/tradfri/translations/en.json
+++ b/homeassistant/components/tradfri/translations/en.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Bridge is already configured.",
- "already_in_progress": "Bridge configuration is already in progress."
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress"
},
"error": {
- "cannot_connect": "Unable to connect to the gateway.",
+ "cannot_connect": "Failed to connect",
"invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.",
"timeout": "Timeout validating the code."
},
diff --git a/homeassistant/components/tradfri/translations/it.json b/homeassistant/components/tradfri/translations/it.json
index 4fe9b2a7c9c841..c05098d97d209f 100644
--- a/homeassistant/components/tradfri/translations/it.json
+++ b/homeassistant/components/tradfri/translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Bridge gi\u00e0 configurato.",
- "already_in_progress": "La configurazione del Bridge \u00e8 gi\u00e0 in corso."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso"
},
"error": {
- "cannot_connect": "Impossibile connettersi al gateway.",
+ "cannot_connect": "Impossibile connettersi",
"invalid_key": "Impossibile registrarsi con la chiave fornita. Se questo continua a succedere, prova a riavviare il gateway.",
"timeout": "Tempo scaduto per la validazione del codice."
},
diff --git a/homeassistant/components/tradfri/translations/no.json b/homeassistant/components/tradfri/translations/no.json
index 39e66e48a78934..3138c472531841 100644
--- a/homeassistant/components/tradfri/translations/no.json
+++ b/homeassistant/components/tradfri/translations/no.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Bridge er allerede konfigurert.",
- "already_in_progress": "Brokonfigurasjon er allerede i gang."
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede"
},
"error": {
- "cannot_connect": "Kan ikke koble til gatewayen.",
+ "cannot_connect": "Tilkobling mislyktes.",
"invalid_key": "Kunne ikke registrere med gitt n\u00f8kkel. Hvis dette fortsetter, pr\u00f8v \u00e5 starte gatewayen p\u00e5 nytt.",
"timeout": "Tidsavbrudd ved validering av kode."
},
diff --git a/homeassistant/components/tradfri/translations/ru.json b/homeassistant/components/tradfri/translations/ru.json
index 03077907aab00c..adb2b3aa18f44f 100644
--- a/homeassistant/components/tradfri/translations/ru.json
+++ b/homeassistant/components/tradfri/translations/ru.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f."
+ "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."
},
"error": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.",
"timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430."
},
diff --git a/homeassistant/components/tradfri/translations/zh-Hant.json b/homeassistant/components/tradfri/translations/zh-Hant.json
index adbb0e148f45dd..e922bb26080c5b 100644
--- a/homeassistant/components/tradfri/translations/zh-Hant.json
+++ b/homeassistant/components/tradfri/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002",
- "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002"
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d"
},
"error": {
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u9598\u9053\u5668\u3002",
diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py
index 958adfd6915f29..bb1bad67f829b6 100644
--- a/homeassistant/components/trafikverket_weatherstation/sensor.py
+++ b/homeassistant/components/trafikverket_weatherstation/sensor.py
@@ -17,6 +17,7 @@
DEGREE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_METERS_PER_SECOND,
TEMP_CELSIUS,
@@ -97,7 +98,7 @@
],
"precipitation_amount": [
"Precipitation amount",
- "mm",
+ LENGTH_MILLIMETERS,
"precipitation_amount",
"mdi:cup-water",
None,
diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py
index 56ed3081b6304f..890a0f3dfa9216 100644
--- a/homeassistant/components/transmission/config_flow.py
+++ b/homeassistant/components/transmission/config_flow.py
@@ -64,13 +64,12 @@ async def async_step_user(self, user_input=None):
if entry.data[CONF_NAME] == user_input[CONF_NAME]:
errors[CONF_NAME] = "name_exists"
break
-
try:
await get_api(self.hass, user_input)
except AuthenticationError:
- errors[CONF_USERNAME] = "wrong_credentials"
- errors[CONF_PASSWORD] = "wrong_credentials"
+ errors[CONF_USERNAME] = "invalid_auth"
+ errors[CONF_PASSWORD] = "invalid_auth"
except (CannotConnect, UnknownError):
errors["base"] = "cannot_connect"
diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json
index 66a5b757818d4e..81725ad7d16d01 100644
--- a/homeassistant/components/transmission/strings.json
+++ b/homeassistant/components/transmission/strings.json
@@ -4,7 +4,7 @@
"user": {
"title": "Setup Transmission Client",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
@@ -13,12 +13,12 @@
}
},
"error": {
- "name_exists": "Name already exists",
- "wrong_credentials": "Wrong username or password",
- "cannot_connect": "Unable to Connect to host"
+ "name_exists": "[%key:common::config_flow::data::name%] already exists",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "Host is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json
index 66d6caf1b1732f..a09fbba4e856cd 100644
--- a/homeassistant/components/transmission/translations/de.json
+++ b/homeassistant/components/transmission/translations/de.json
@@ -25,6 +25,7 @@
"step": {
"init": {
"data": {
+ "limit": "Limit",
"order": "Reihenfolge",
"scan_interval": "Aktualisierungsfrequenz"
},
diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json
new file mode 100644
index 00000000000000..d7f39519aad421
--- /dev/null
+++ b/homeassistant/components/transmission/translations/et.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nimi",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 0f758d4a2ebee0..7ee4922677cbad 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -11,6 +11,7 @@
from aiohttp import web
import mutagen
+from mutagen.id3 import ID3FileType, TextFrame as ID3Text
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -465,11 +466,16 @@ def write_tags(filename, data, provider, message, language, options):
artist = options.get("voice")
try:
- tts_file = mutagen.File(data_bytes, easy=True)
+ tts_file = mutagen.File(data_bytes)
if tts_file is not None:
- tts_file["artist"] = artist
- tts_file["album"] = album
- tts_file["title"] = message
+ if isinstance(tts_file, ID3FileType):
+ tts_file["artist"] = ID3Text(encoding=3, text=artist)
+ tts_file["album"] = ID3Text(encoding=3, text=album)
+ tts_file["title"] = ID3Text(encoding=3, text=message)
+ else:
+ tts_file["artist"] = artist
+ tts_file["album"] = album
+ tts_file["title"] = message
tts_file.save(data_bytes)
except mutagen.MutagenError as err:
_LOGGER.error("ID3 tag error: %s", err)
diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py
index c905334aac78d5..f00396d4405b2f 100644
--- a/homeassistant/components/tuya/config_flow.py
+++ b/homeassistant/components/tuya/config_flow.py
@@ -22,8 +22,8 @@
}
)
-RESULT_AUTH_FAILED = "auth_failed"
-RESULT_CONN_ERROR = "conn_error"
+RESULT_AUTH_FAILED = "invalid_auth"
+RESULT_CONN_ERROR = "cannot_connect"
RESULT_SUCCESS = "success"
RESULT_LOG_MESSAGE = {
diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json
index 891eeb4764343a..08123db3a36a6c 100644
--- a/homeassistant/components/tuya/strings.json
+++ b/homeassistant/components/tuya/strings.json
@@ -14,12 +14,12 @@
}
},
"abort": {
- "auth_failed": "[%key:common::config_flow::error::invalid_auth%]",
- "conn_error": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {
- "auth_failed": "[%key:common::config_flow::error::invalid_auth%]"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}
}
}
diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json
index 20709b61a98ebb..ca88e8b54a42a7 100644
--- a/homeassistant/components/tuya/translations/ca.json
+++ b/homeassistant/components/tuya/translations/ca.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "Autenticaci\u00f3 inv\u00e0lida",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"conn_error": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
- "auth_failed": "Autenticaci\u00f3 inv\u00e0lida"
+ "auth_failed": "Autenticaci\u00f3 inv\u00e0lida",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
},
"flow_title": "Configuraci\u00f3 de Tuya",
"step": {
diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json
index c66eb2d274fbba..8b013c6c0627c2 100644
--- a/homeassistant/components/tuya/translations/en.json
+++ b/homeassistant/components/tuya/translations/en.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "Invalid authentication",
+ "cannot_connect": "Failed to connect",
"conn_error": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
- "auth_failed": "Invalid authentication"
+ "auth_failed": "Invalid authentication",
+ "invalid_auth": "Invalid authentication"
},
"flow_title": "Tuya configuration",
"step": {
diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json
index f0a607331f39de..b11fb8101b0eca 100644
--- a/homeassistant/components/tuya/translations/es.json
+++ b/homeassistant/components/tuya/translations/es.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "Autenticaci\u00f3n no v\u00e1lida",
+ "cannot_connect": "No se pudo conectar",
"conn_error": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
- "auth_failed": "Autenticaci\u00f3n no v\u00e1lida"
+ "auth_failed": "Autenticaci\u00f3n no v\u00e1lida",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
},
"flow_title": "Configuraci\u00f3n Tuya",
"step": {
diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json
new file mode 100644
index 00000000000000..7d4781fa23e05e
--- /dev/null
+++ b/homeassistant/components/tuya/translations/et.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "auth_failed": "Viga tuvastamisel",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "conn_error": "\u00dchendamine eba\u00f5nnestus",
+ "invalid_auth": "Tuvastamise viga",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine."
+ },
+ "error": {
+ "auth_failed": "Vigane tuvastamine",
+ "invalid_auth": "Tuvastamise viga"
+ },
+ "flow_title": "Tuya seaded",
+ "step": {
+ "user": {
+ "data": {
+ "country_code": "Teie konto riigikood (nt 1 USA v\u00f5i 372 Eesti)",
+ "password": "Salas\u00f5na",
+ "platform": "\u00c4pp kus teie konto registreeriti",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisestage oma Tuya konto andmed."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json
index fc2c8fc49b337e..027764d614fb80 100644
--- a/homeassistant/components/tuya/translations/it.json
+++ b/homeassistant/components/tuya/translations/it.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "Autenticazione non valida",
+ "cannot_connect": "Impossibile connettersi",
"conn_error": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
- "auth_failed": "Autenticazione non valida"
+ "auth_failed": "Autenticazione non valida",
+ "invalid_auth": "Autenticazione non valida"
},
"flow_title": "Configurazione di Tuya",
"step": {
diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json
index 5681f95d9842f8..76994d91436d46 100644
--- a/homeassistant/components/tuya/translations/no.json
+++ b/homeassistant/components/tuya/translations/no.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "Ugyldig godkjenning",
+ "cannot_connect": "Tilkobling mislyktes.",
"conn_error": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
- "auth_failed": "Ugyldig godkjenning"
+ "auth_failed": "Ugyldig godkjenning",
+ "invalid_auth": "Ugyldig godkjenning"
},
"flow_title": "Tuya konfigurasjon",
"step": {
diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json
index 7278806b5f6b93..d306172fab0edb 100644
--- a/homeassistant/components/tuya/translations/pl.json
+++ b/homeassistant/components/tuya/translations/pl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "auth_failed": "Niepoprawne uwierzytelnienie.",
- "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "auth_failed": "Niepoprawne uwierzytelnienie",
+ "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
- "auth_failed": "Niepoprawne uwierzytelnienie."
+ "auth_failed": "Niepoprawne uwierzytelnienie"
},
"flow_title": "Konfiguracja integracji Tuya",
"step": {
diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json
index 4eedc62396ea01..bdb3ad91c358e4 100644
--- a/homeassistant/components/tuya/translations/ru.json
+++ b/homeassistant/components/tuya/translations/ru.json
@@ -2,11 +2,14 @@
"config": {
"abort": {
"auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"conn_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"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": {
- "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
},
"flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya",
"step": {
diff --git a/homeassistant/components/twentemilieu/translations/et.json b/homeassistant/components/twentemilieu/translations/et.json
new file mode 100644
index 00000000000000..ddaa8004c97b9f
--- /dev/null
+++ b/homeassistant/components/twentemilieu/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "\u00dchenduse loomine nurjus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json
index bfa38f9ef8a529..e654bbac6a4987 100644
--- a/homeassistant/components/twentemilieu/translations/pl.json
+++ b/homeassistant/components/twentemilieu/translations/pl.json
@@ -4,7 +4,7 @@
"address_exists": "Adres jest ju\u017c skonfigurowany."
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu."
},
"step": {
diff --git a/homeassistant/components/twilio/strings.json b/homeassistant/components/twilio/strings.json
index 96e0249df9a8b1..0480fdae7c890e 100644
--- a/homeassistant/components/twilio/strings.json
+++ b/homeassistant/components/twilio/strings.json
@@ -7,7 +7,7 @@
}
},
"abort": {
- "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages."
},
"create_entry": {
diff --git a/homeassistant/components/twilio/translations/ca.json b/homeassistant/components/twilio/translations/ca.json
index 591124fda28606..b4adcdb1cb49a9 100644
--- a/homeassistant/components/twilio/translations/ca.json
+++ b/homeassistant/components/twilio/translations/ca.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.",
- "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia.",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants."
diff --git a/homeassistant/components/twilio/translations/el.json b/homeassistant/components/twilio/translations/el.json
new file mode 100644
index 00000000000000..aecb2ee553fa42
--- /dev/null
+++ b/homeassistant/components/twilio/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/en.json b/homeassistant/components/twilio/translations/en.json
index 32df9de104da5d..de5d921dec2b0c 100644
--- a/homeassistant/components/twilio/translations/en.json
+++ b/homeassistant/components/twilio/translations/en.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.",
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Only a single instance is necessary.",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json
index a470b3619a4975..8e5ade47c7bf1a 100644
--- a/homeassistant/components/twilio/translations/es.json
+++ b/homeassistant/components/twilio/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Twilio.",
- "one_instance_allowed": "S\u00f3lo se necesita una instancia."
+ "one_instance_allowed": "S\u00f3lo se necesita una instancia.",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes."
diff --git a/homeassistant/components/twilio/translations/et.json b/homeassistant/components/twilio/translations/et.json
new file mode 100644
index 00000000000000..e8b5eae41495c6
--- /dev/null
+++ b/homeassistant/components/twilio/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/fr.json b/homeassistant/components/twilio/translations/fr.json
index 2144fdd1d48425..3136fb5397415e 100644
--- a/homeassistant/components/twilio/translations/fr.json
+++ b/homeassistant/components/twilio/translations/fr.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages Twilio.",
- "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire.",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Twilio] ( {twilio_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / x-www-form-urlencoded \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes."
diff --git a/homeassistant/components/twilio/translations/it.json b/homeassistant/components/twilio/translations/it.json
index 8bf92bf60a2b81..ee7ed374cea157 100644
--- a/homeassistant/components/twilio/translations/it.json
+++ b/homeassistant/components/twilio/translations/it.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Twilio.",
- "one_instance_allowed": "\u00c8 necessaria una sola istanza."
+ "one_instance_allowed": "\u00c8 necessaria una sola istanza.",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"create_entry": {
"default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo."
diff --git a/homeassistant/components/twilio/translations/lb.json b/homeassistant/components/twilio/translations/lb.json
index 8f741409059283..5183c9f12fd8ac 100644
--- a/homeassistant/components/twilio/translations/lb.json
+++ b/homeassistant/components/twilio/translations/lb.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Twilio Noriichten z'empf\u00e4nken.",
- "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg.",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Twilio]({twilio_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
diff --git a/homeassistant/components/twilio/translations/no.json b/homeassistant/components/twilio/translations/no.json
index 98b2575d691842..b6b70d39d7d8ab 100644
--- a/homeassistant/components/twilio/translations/no.json
+++ b/homeassistant/components/twilio/translations/no.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Twilio-meldinger.",
- "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig."
+ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig.",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"create_entry": {
"default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Twilio]({twilio_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/x-www-form-urlencoded \n\n Se [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data."
diff --git a/homeassistant/components/twilio/translations/ru.json b/homeassistant/components/twilio/translations/ru.json
index cdb9377cd998fa..74972ab9b08282 100644
--- a/homeassistant/components/twilio/translations/ru.json
+++ b/homeassistant/components/twilio/translations/ru.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Twilio.",
- "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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."
},
"create_entry": {
"default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\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]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445."
diff --git a/homeassistant/components/twilio/translations/zh-Hant.json b/homeassistant/components/twilio/translations/zh-Hant.json
index cc198a4f43d7f1..1f07a1f44e10d2 100644
--- a/homeassistant/components/twilio/translations/zh-Hant.json
+++ b/homeassistant/components/twilio/translations/zh-Hant.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002",
- "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"create_entry": {
"default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Twilio]({twilio_url})\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002"
diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py
index 52212fca0602f1..4b0196281584e4 100644
--- a/homeassistant/components/twitch/sensor.py
+++ b/homeassistant/components/twitch/sensor.py
@@ -72,11 +72,6 @@ def __init__(self, channel, client):
self._follow = None
self._statistics = None
- @property
- def should_poll(self):
- """Device should be polled."""
- return True
-
@property
def name(self):
"""Return the name of the sensor."""
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index 6115821b00093a..e40fb30a62c855 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -16,6 +16,7 @@
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -312,7 +313,11 @@ async def async_step_statistics_sensors(self, user_input=None):
vol.Optional(
CONF_ALLOW_BANDWIDTH_SENSORS,
default=self.controller.option_allow_bandwidth_sensors,
- ): bool
+ ): bool,
+ vol.Optional(
+ CONF_ALLOW_UPTIME_SENSORS,
+ default=self.controller.option_allow_uptime_sensors,
+ ): bool,
}
),
)
diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py
index 803a892647fe0e..42d160f2dea0fd 100644
--- a/homeassistant/components/unifi/const.py
+++ b/homeassistant/components/unifi/const.py
@@ -12,6 +12,7 @@
UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients"
CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors"
+CONF_ALLOW_UPTIME_SENSORS = "allow_uptime_sensors"
CONF_BLOCK_CLIENT = "block_client"
CONF_DETECTION_TIME = "detection_time"
CONF_IGNORE_WIRED_BUG = "ignore_wired_bug"
@@ -22,6 +23,7 @@
CONF_SSID_FILTER = "ssid_filter"
DEFAULT_ALLOW_BANDWIDTH_SENSORS = False
+DEFAULT_ALLOW_UPTIME_SENSORS = False
DEFAULT_IGNORE_WIRED_BUG = False
DEFAULT_POE_CLIENTS = True
DEFAULT_TRACK_CLIENTS = True
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 7c30a34f58f411..6fc5b3d9ed7d93 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -33,6 +33,7 @@
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -45,6 +46,7 @@
CONF_TRACK_WIRED_CLIENTS,
CONTROLLER_ID,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
+ DEFAULT_ALLOW_UPTIME_SENSORS,
DEFAULT_DETECTION_TIME,
DEFAULT_IGNORE_WIRED_BUG,
DEFAULT_POE_CLIENTS,
@@ -184,6 +186,13 @@ def option_allow_bandwidth_sensors(self):
CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS
)
+ @property
+ def option_allow_uptime_sensors(self):
+ """Config entry option to allow uptime sensors."""
+ return self.config_entry.options.get(
+ CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS
+ )
+
@callback
def async_unifi_signalling_callback(self, signal, data):
"""Handle messages back from UniFi library."""
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 831a833552893d..4c5d3309c08aa7 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -233,9 +233,7 @@ def unique_id(self) -> str:
@property
def device_state_attributes(self):
"""Return the client state attributes."""
- attributes = {}
-
- attributes["is_wired"] = self.is_wired
+ attributes = {"is_wired": self.is_wired}
if self.is_connected:
for variable in CLIENT_CONNECTED_ATTRIBUTES:
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
index 8fdb0ac14611bb..59aff09811fff2 100644
--- a/homeassistant/components/unifi/sensor.py
+++ b/homeassistant/components/unifi/sensor.py
@@ -1,10 +1,11 @@
"""Support for bandwidth sensors with UniFi clients."""
import logging
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN
from homeassistant.const import DATA_MEGABYTES
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+import homeassistant.util.dt as dt_util
from .const import DOMAIN as UNIFI_DOMAIN
from .unifi_client import UniFiClient
@@ -13,6 +14,7 @@
RX_SENSOR = "rx"
TX_SENSOR = "tx"
+UPTIME_SENSOR = "uptime"
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -22,7 +24,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for UniFi integration."""
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- controller.entities[DOMAIN] = {RX_SENSOR: set(), TX_SENSOR: set()}
+ controller.entities[DOMAIN] = {
+ RX_SENSOR: set(),
+ TX_SENSOR: set(),
+ UPTIME_SENSOR: set(),
+ }
@callback
def items_added(
@@ -30,7 +36,10 @@ def items_added(
) -> None:
"""Update the values of the controller."""
if controller.option_allow_bandwidth_sensors:
- add_entities(controller, async_add_entities, clients)
+ add_bandwith_entities(controller, async_add_entities, clients)
+
+ if controller.option_allow_uptime_sensors:
+ add_uptime_entities(controller, async_add_entities, clients)
for signal in (controller.signal_update, controller.signal_options_update):
controller.listeners.append(async_dispatcher_connect(hass, signal, items_added))
@@ -39,7 +48,7 @@ def items_added(
@callback
-def add_entities(controller, async_add_entities, clients):
+def add_bandwith_entities(controller, async_add_entities, clients):
"""Add new sensor entities from the controller."""
sensors = []
@@ -55,6 +64,22 @@ def add_entities(controller, async_add_entities, clients):
async_add_entities(sensors)
+@callback
+def add_uptime_entities(controller, async_add_entities, clients):
+ """Add new sensor entities from the controller."""
+ sensors = []
+
+ for mac in clients:
+ if mac in controller.entities[DOMAIN][UniFiUpTimeSensor.TYPE]:
+ continue
+
+ client = controller.api.clients[mac]
+ sensors.append(UniFiUpTimeSensor(client, controller))
+
+ if sensors:
+ async_add_entities(sensors)
+
+
class UniFiBandwidthSensor(UniFiClient):
"""UniFi bandwidth sensor base class."""
@@ -100,3 +125,30 @@ def state(self) -> int:
if self._is_wired:
return self.client.wired_tx_bytes / 1000000
return self.client.tx_bytes / 1000000
+
+
+class UniFiUpTimeSensor(UniFiClient):
+ """UniFi uptime sensor."""
+
+ DOMAIN = DOMAIN
+ TYPE = UPTIME_SENSOR
+
+ @property
+ def device_class(self) -> str:
+ """Return device class."""
+ return DEVICE_CLASS_TIMESTAMP
+
+ @property
+ def name(self) -> str:
+ """Return the name of the client."""
+ return f"{super().name} {self.TYPE.capitalize()}"
+
+ @property
+ def state(self) -> int:
+ """Return the uptime of the client."""
+ return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat()
+
+ async def options_updated(self) -> None:
+ """Config entry options are updated, remove entity if option is disabled."""
+ if not self.controller.option_allow_uptime_sensors:
+ await self.remove_item({self.client.mac})
diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json
index 95d273278bd672..9deb68f4e3bb5c 100644
--- a/homeassistant/components/unifi/strings.json
+++ b/homeassistant/components/unifi/strings.json
@@ -9,7 +9,7 @@
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"site": "Site ID",
- "verify_ssl": "Controller using proper certificate"
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
+ "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients",
+ "allow_uptime_sensors": "Uptime sensors for network clients"
},
"description": "Configure statistics sensors",
"title": "UniFi options 3/3"
diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json
index de2a8ffc562837..fb52e583d365fb 100644
--- a/homeassistant/components/unifi/translations/ca.json
+++ b/homeassistant/components/unifi/translations/ca.json
@@ -16,7 +16,7 @@
"port": "[%key::common::config_flow::data::port%]",
"site": "ID del lloc",
"username": "[%key::common::config_flow::data::username%]",
- "verify_ssl": "El controlador est\u00e0 utilitzant un certificat adequat"
+ "verify_ssl": "Verifica el certificat SSL"
},
"title": "Configuraci\u00f3 del controlador UniFi"
}
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Sensors d'utilitzaci\u00f3 d'ample de banda per a clients de la xarxa"
+ "allow_bandwidth_sensors": "Sensors d'utilitzaci\u00f3 d'ample de banda per a clients de la xarxa",
+ "allow_uptime_sensors": "Sensors de temps d'activitat per a clients de xarxa"
},
"description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques",
"title": "Opcions d'UniFi 3/3"
diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json
index a2adfcce3084fd..f36ba9ac5ad79d 100644
--- a/homeassistant/components/unifi/translations/cs.json
+++ b/homeassistant/components/unifi/translations/cs.json
@@ -15,8 +15,7 @@
"password": "Heslo",
"port": "Port",
"site": "ID s\u00edt\u011b",
- "username": "U\u017eivatelsk\u00e9 jm\u00e9no",
- "verify_ssl": "Ovlada\u010d pou\u017e\u00edv\u00e1 spr\u00e1vn\u00fd certifik\u00e1t"
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
},
"title": "Nastaven\u00ed UniFi ovlada\u010de"
}
@@ -49,7 +48,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro p\u0159ipojen\u00e9 klienty"
+ "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro p\u0159ipojen\u00e9 klienty",
+ "allow_uptime_sensors": "Vytvo\u0159it senzory doby provozuschopnosti pro s\u00ed\u0165ov\u00e9 klienty"
},
"description": "Konfigurovat statistick\u00e9 senzory",
"title": "Mo\u017enosti UniFi 3/3"
diff --git a/homeassistant/components/unifi/translations/el.json b/homeassistant/components/unifi/translations/el.json
new file mode 100644
index 00000000000000..e6b521c1543360
--- /dev/null
+++ b/homeassistant/components/unifi/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "statistics_sensors": {
+ "data": {
+ "allow_uptime_sensors": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b5\u03c7\u03bf\u03cd\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json
index 691f4fb6b01684..ed3a26b335a750 100644
--- a/homeassistant/components/unifi/translations/en.json
+++ b/homeassistant/components/unifi/translations/en.json
@@ -16,7 +16,7 @@
"port": "Port",
"site": "Site ID",
"username": "Username",
- "verify_ssl": "Controller using proper certificate"
+ "verify_ssl": "Verify SSL certificate"
},
"title": "Set up UniFi Controller"
}
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
+ "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients",
+ "allow_uptime_sensors": "Uptime sensors for network clients"
},
"description": "Configure statistics sensors",
"title": "UniFi options 3/3"
diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json
index da67cd1bcafee4..35b1d3d6e299cc 100644
--- a/homeassistant/components/unifi/translations/es.json
+++ b/homeassistant/components/unifi/translations/es.json
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red"
+ "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red",
+ "allow_uptime_sensors": "Sensores de tiempo de actividad para clientes de la red"
},
"description": "Configurar estad\u00edsticas de los sensores",
"title": "Opciones UniFi 3/3"
diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json
index 21d422ace4d49e..03a872a8f96b46 100644
--- a/homeassistant/components/unifi/translations/fr.json
+++ b/homeassistant/components/unifi/translations/fr.json
@@ -26,13 +26,16 @@
"step": {
"client_control": {
"data": {
- "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau"
+ "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau",
+ "poe_clients": "Autoriser le contr\u00f4le POE des clients"
},
+ "description": "Configurer les contr\u00f4les client \n\n Cr\u00e9ez des interrupteurs pour les num\u00e9ros de s\u00e9rie pour lesquels vous souhaitez contr\u00f4ler l'acc\u00e8s au r\u00e9seau.",
"title": "Options UniFi 2/3"
},
"device_tracker": {
"data": {
"detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent",
+ "ignore_wired_bug": "D\u00e9sactiver la logique de bogue filaire UniFi",
"ssid_filter": "S\u00e9lectionnez les SSID pour suivre les clients sans fil",
"track_clients": "Suivre les clients du r\u00e9seau",
"track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)",
@@ -57,7 +60,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau"
+ "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau",
+ "allow_uptime_sensors": "Capteurs de disponibilit\u00e9 pour les clients r\u00e9seau"
},
"description": "Configurer des capteurs de statistiques",
"title": "Options UniFi 3/3"
diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json
index 8a06ca440c5573..91326796740db7 100644
--- a/homeassistant/components/unifi/translations/it.json
+++ b/homeassistant/components/unifi/translations/it.json
@@ -16,7 +16,7 @@
"port": "Porta",
"site": "ID del sito",
"username": "Nome utente",
- "verify_ssl": "Il Controller sta utilizzando il certificato corretto"
+ "verify_ssl": "Verificare il certificato SSL"
},
"title": "Configura l'UniFi Controller"
}
@@ -60,7 +60,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete"
+ "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete",
+ "allow_uptime_sensors": "Sensori di tempo di funzionamento per i client di rete"
},
"description": "Configurare i sensori delle statistiche",
"title": "Opzioni UniFi 3/3"
diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json
index a3d2c8f3b69c74..94160829bad4c6 100644
--- a/homeassistant/components/unifi/translations/ko.json
+++ b/homeassistant/components/unifi/translations/ko.json
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c"
+ "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c",
+ "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c"
},
"description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131",
"title": "UniFi \uc635\uc158 3/3"
diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json
index 5f7e7192bd95b9..992ee8192e3fc4 100644
--- a/homeassistant/components/unifi/translations/lb.json
+++ b/homeassistant/components/unifi/translations/lb.json
@@ -53,7 +53,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente"
+ "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente",
+ "allow_uptime_sensors": "Uptime Sensoren fir Netzwierkklienten"
},
"description": "Statistik Sensoren konfigur\u00e9ieren",
"title": "UniFi Optiounen 3/3"
diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json
index 5d64d73d1deca6..f945e5c4d6da3a 100644
--- a/homeassistant/components/unifi/translations/nl.json
+++ b/homeassistant/components/unifi/translations/nl.json
@@ -36,6 +36,7 @@
"data": {
"detection_time": "Tijd in seconden vanaf laatst gezien tot beschouwd als weg",
"ignore_wired_bug": "Schakel UniFi bedrade buglogica uit",
+ "ssid_filter": "Selecteer SSID's om draadloze clients op te volgen",
"track_clients": "Volg netwerkclients",
"track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)",
"track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten"
@@ -57,8 +58,10 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients"
+ "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients",
+ "allow_uptime_sensors": "Uptime-sensoren voor netwerkclients"
},
+ "description": "Configureer statistische sensoren",
"title": "UniFi-opties 3/3"
}
}
diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json
index a861790ba8d876..29e13a40182e18 100644
--- a/homeassistant/components/unifi/translations/no.json
+++ b/homeassistant/components/unifi/translations/no.json
@@ -16,7 +16,7 @@
"port": "",
"site": "Nettsted-ID",
"username": "Brukernavn",
- "verify_ssl": "Kontroller bruker riktig sertifikat"
+ "verify_ssl": "Verifisere SSL-sertifikat"
},
"title": "Sett opp UniFi kontroller"
}
@@ -60,7 +60,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter"
+ "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter",
+ "allow_uptime_sensors": "Oppetidssensorer for nettverksklienter"
},
"description": "Konfigurer statistikk sensorer",
"title": "UniFi-alternativ 3/3"
diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json
index c062d3929113f0..079b9933bace94 100644
--- a/homeassistant/components/unifi/translations/pl.json
+++ b/homeassistant/components/unifi/translations/pl.json
@@ -4,8 +4,8 @@
"already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana."
},
"error": {
- "faulty_credentials": "Niepoprawne uwierzytelnienie.",
- "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "faulty_credentials": "Niepoprawne uwierzytelnienie",
+ "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"unknown_client_mac": "Brak klienta z tym adresem MAC"
},
"step": {
@@ -62,7 +62,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych"
+ "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych",
+ "allow_uptime_sensors": "Sensory czasu pracy dla klient\u00f3w sieciowych"
},
"description": "Konfiguracja sensora statystyk",
"title": "Opcje UniFi"
diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json
index 550867b682b479..9096408474aec5 100644
--- a/homeassistant/components/unifi/translations/ru.json
+++ b/homeassistant/components/unifi/translations/ru.json
@@ -16,7 +16,7 @@
"port": "\u041f\u043e\u0440\u0442",
"site": "ID \u0441\u0430\u0439\u0442\u0430",
"username": "\u041b\u043e\u0433\u0438\u043d",
- "verify_ssl": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "UniFi Controller"
}
@@ -62,7 +62,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432"
+ "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432",
+ "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 3"
diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json
index 7ba51c9b621d2f..1ed8d8d8856f8e 100644
--- a/homeassistant/components/unifi/translations/zh-Hant.json
+++ b/homeassistant/components/unifi/translations/zh-Hant.json
@@ -16,7 +16,7 @@
"port": "\u901a\u8a0a\u57e0",
"site": "\u4f4d\u5740 ID",
"username": "\u4f7f\u7528\u8005\u540d\u7a31",
- "verify_ssl": "\u63a7\u5236\u5668\u4f7f\u7528\u9a57\u8b49"
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"title": "\u8a2d\u5b9a UniFi \u63a7\u5236\u5668"
}
@@ -54,7 +54,8 @@
},
"statistics_sensors": {
"data": {
- "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668"
+ "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668",
+ "allow_uptime_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u4e0a\u7dda\u6642\u9593\u611f\u6e2c\u5668"
},
"description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668",
"title": "UniFi \u9078\u9805 3/3"
diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py
index c38afc139cf626..aedc27c2a29fc7 100644
--- a/homeassistant/components/universal/media_player.py
+++ b/homeassistant/components/universal/media_player.py
@@ -145,8 +145,9 @@ async def async_added_to_hass(self):
"""Subscribe to children and template state changes."""
@callback
- def _async_on_dependency_update(*_):
+ def _async_on_dependency_update(event):
"""Update ha state when dependencies update."""
+ self.async_set_context(event.context)
self.async_schedule_update_ha_state(True)
@callback
@@ -158,6 +159,10 @@ def _async_on_template_update(event, updates):
self._state_template_result = None
else:
self._state_template_result = result
+
+ if event:
+ self.async_set_context(event.context)
+
self.async_schedule_update_ha_state(True)
if self._state_template is not None:
diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py
index a84b71c71e4d05..3af9999bd9fe36 100644
--- a/homeassistant/components/upb/config_flow.py
+++ b/homeassistant/components/upb/config_flow.py
@@ -89,7 +89,7 @@ async def async_step_user(self, user_input=None):
if user_input is not None:
try:
if self._url_already_configured(_make_url_from_data(user_input)):
- return self.async_abort(reason="address_already_configured")
+ return self.async_abort(reason="already_configured")
network_id, info = await _validate_input(user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
diff --git a/homeassistant/components/upb/strings.json b/homeassistant/components/upb/strings.json
index fb4f82d555e6b9..9b2cc0a1b12f16 100644
--- a/homeassistant/components/upb/strings.json
+++ b/homeassistant/components/upb/strings.json
@@ -12,12 +12,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect to UPB PIM, please try again.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_upb_file": "Missing or invalid UPB UPStart export file, check the name and path of the file.",
- "unknown": "Unexpected error."
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "address_already_configured": "An UPB PIM with this address is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
diff --git a/homeassistant/components/upb/translations/et.json b/homeassistant/components/upb/translations/et.json
new file mode 100644
index 00000000000000..8bf3b2170b4794
--- /dev/null
+++ b/homeassistant/components/upb/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Ootamatu t\u00f5rge."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upb/translations/pl.json b/homeassistant/components/upb/translations/pl.json
index fcc6fb8bead184..ba19cd38cd213f 100644
--- a/homeassistant/components/upb/translations/pl.json
+++ b/homeassistant/components/upb/translations/pl.json
@@ -1,12 +1,17 @@
{
"config": {
+ "abort": {
+ "address_already_configured": "UPB PIM z takim adresem jest ju\u017c skonfigurowany."
+ },
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z UPB PIM, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "invalid_upb_file": "Brak lub nieprawid\u0142owy plik eksportu UPB UPStart, sprawd\u017a nazw\u0119 i \u015bcie\u017ck\u0119 do pliku.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
"data": {
+ "file_path": "\u015acie\u017cka i nazwa pliku eksportu UPStart UPB.",
"protocol": "Protok\u00f3\u0142"
}
}
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 52cada89333ff8..773a872f33fbd5 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -56,7 +56,9 @@ async def async_discover_and_construct(
filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st]
if not filtered:
_LOGGER.warning(
- 'Wanted UPnP/IGD device with UDN "%s" not found, aborting', udn
+ 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting',
+ udn,
+ st,
)
return None
@@ -104,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
"""Set up UPnP/IGD device from a config entry."""
_LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data)
- # discover and construct
+ # Discover and construct.
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name
try:
@@ -116,11 +118,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
_LOGGER.info("Unable to create UPnP/IGD, aborting")
raise ConfigEntryNotReady
- # Save device
+ # Save device.
hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device
- # Ensure entry has proper unique_id.
- if config_entry.unique_id != device.unique_id:
+ # Ensure entry has a unique_id.
+ if not config_entry.unique_id:
hass.config_entries.async_update_entry(
entry=config_entry,
unique_id=device.unique_id,
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 016a5a25017264..72efc4ffd554a3 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -54,7 +54,7 @@ async def async_step_user(self, user_input: Optional[Mapping] = None):
if discovery[DISCOVERY_USN] == user_input["usn"]
]
if not matching_discoveries:
- return self.async_abort(reason="no_devices_discovered")
+ return self.async_abort(reason="no_devices_found")
discovery = matching_discoveries[0]
await self.async_set_unique_id(
@@ -104,19 +104,10 @@ async def async_step_import(self, import_info: Optional[Mapping]):
"""
_LOGGER.debug("async_step_import: import_info: %s", import_info)
- if import_info is None:
- # Landed here via configuration.yaml entry.
- # Any device already added, then abort.
- if self._async_current_entries():
- _LOGGER.debug("aborting, already configured")
- return self.async_abort(reason="already_configured")
-
- # Test if import_info isn't already configured.
- if import_info is not None and any(
- import_info["udn"] == entry.data[CONFIG_ENTRY_UDN]
- and import_info["st"] == entry.data[CONFIG_ENTRY_ST]
- for entry in self._async_current_entries()
- ):
+ # Landed here via configuration.yaml entry.
+ # Any device already added, then abort.
+ if self._async_current_entries():
+ _LOGGER.debug("Already configured, aborting")
return self.async_abort(reason="already_configured")
# Discover devices.
@@ -127,8 +118,17 @@ async def async_step_import(self, import_info: Optional[Mapping]):
_LOGGER.info("No UPnP devices discovered, aborting")
return self.async_abort(reason="no_devices_found")
- discovery = self._discoveries[0]
- return await self._async_create_entry_from_discovery(discovery)
+ # Ensure complete discovery.
+ discovery_info = self._discoveries[0]
+ if DISCOVERY_USN not in discovery_info:
+ _LOGGER.debug("Incomplete discovery, ignoring")
+ return self.async_abort(reason="incomplete_discovery")
+
+ # Ensure not already configuring/configured.
+ usn = discovery_info[DISCOVERY_USN]
+ await self.async_set_unique_id(usn)
+
+ return await self._async_create_entry_from_discovery(discovery_info)
async def async_step_ssdp(self, discovery_info: Mapping):
"""Handle a discovered UPnP/IGD device.
@@ -191,7 +191,7 @@ async def _async_create_entry_from_discovery(
):
"""Create an entry from discovery."""
_LOGGER.debug(
- "_async_create_entry_from_data: discovery: %s",
+ "_async_create_entry_from_discovery: discovery: %s",
discovery,
)
# Get name from device, if not found already.
diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py
index c4a81db1ff44a6..5f29043a1fe84e 100644
--- a/homeassistant/components/upnp/device.py
+++ b/homeassistant/components/upnp/device.py
@@ -67,7 +67,7 @@ async def async_create_device(cls, hass: HomeAssistantType, ssdp_location: str):
"""Create UPnP/IGD device."""
# build async_upnp_client requester
session = async_get_clientsession(hass)
- requester = AiohttpSessionRequester(session, True)
+ requester = AiohttpSessionRequester(session, True, 10)
# create async_upnp_client device
factory = UpnpFactory(requester, disable_state_variable_validation=True)
diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json
index 99e58698f2e5b5..97e91c490e3893 100644
--- a/homeassistant/components/upnp/strings.json
+++ b/homeassistant/components/upnp/strings.json
@@ -15,9 +15,8 @@
}
},
"abort": {
- "already_configured": "UPnP/IGD is already configured",
- "no_devices_discovered": "No UPnP/IGDs discovered",
- "no_devices_found": "No UPnP/IGD devices found on the network.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"incomplete_discovery": "Incomplete discovery"
}
}
diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json
new file mode 100644
index 00000000000000..76145d6e6e164d
--- /dev/null
+++ b/homeassistant/components/upnp/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "UPnP / IGD on juba seadistatud",
+ "incomplete_discovery": "Mittet\u00e4ielik avastamine",
+ "no_devices_discovered": "\u00dchtegi UPnP / IGD-d ei avastatud",
+ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi UPnP / IGD-seadet."
+ },
+ "error": {
+ "one": "\u00fcks",
+ "other": "Teine"
+ },
+ "flow_title": "UPnP / IGD: {name}",
+ "step": {
+ "init": {
+ "one": "\u00dcks",
+ "other": "Teine"
+ },
+ "ssdp_confirm": {
+ "description": "Kas soovite UPnP / IGD seadme seadistada?"
+ },
+ "user": {
+ "data": {
+ "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)",
+ "usn": "Seade"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py
index 231dce9a402ef1..e31d8b44b100fa 100644
--- a/homeassistant/components/uptimerobot/binary_sensor.py
+++ b/homeassistant/components/uptimerobot/binary_sensor.py
@@ -4,7 +4,11 @@
from pyuptimerobot import UptimeRobot
import voluptuous as vol
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ PLATFORM_SCHEMA,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
@@ -68,7 +72,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return "connectivity"
+ return DEVICE_CLASS_CONNECTIVITY
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py
index 5be7dcf9b6974c..7b55ec4dcd0f1d 100644
--- a/homeassistant/components/utility_meter/const.py
+++ b/homeassistant/components/utility_meter/const.py
@@ -5,10 +5,11 @@
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
+BIMONTHLY = "bimonthly"
QUARTERLY = "quarterly"
YEARLY = "yearly"
-METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]
+METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY]
DATA_UTILITY = "utility_meter_data"
diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py
index 8372d8e6b22bf2..54f93422abd377 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -24,6 +24,7 @@
from .const import (
ATTR_VALUE,
+ BIMONTHLY,
CONF_METER,
CONF_METER_NET_CONSUMPTION,
CONF_METER_OFFSET,
@@ -204,6 +205,12 @@ async def _async_reset_meter(self, event):
and now != date(now.year, now.month, 1) + self._period_offset
):
return
+ if (
+ self._period == BIMONTHLY
+ and now
+ != date(now.year, (((now.month - 1) // 2) * 2 + 1), 1) + self._period_offset
+ ):
+ return
if (
self._period == QUARTERLY
and now
@@ -241,7 +248,7 @@ async def async_added_to_hass(self):
minute=self._period_offset.seconds // 60,
second=self._period_offset.seconds % 60,
)
- elif self._period in [DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]:
+ elif self._period in [DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY]:
async_track_time_change(
self.hass,
self._async_reset_meter,
diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py
index e07fac28d1f6a1..da748c20c1cfb3 100644
--- a/homeassistant/components/uvc/camera.py
+++ b/homeassistant/components/uvc/camera.py
@@ -152,7 +152,7 @@ def _login(self):
camera.login()
_LOGGER.debug(
"Logged into UVC camera %(name)s via %(addr)s",
- dict(name=self._name, addr=addr),
+ {"name": self._name, "addr": addr},
)
self._connect_addr = addr
break
diff --git a/homeassistant/components/vacuum/group.py b/homeassistant/components/vacuum/group.py
new file mode 100644
index 00000000000000..0219ecdf795796
--- /dev/null
+++ b/homeassistant/components/vacuum/group.py
@@ -0,0 +1,19 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import STATE_CLEANING, STATE_ERROR, STATE_RETURNING
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states(
+ {STATE_CLEANING, STATE_ON, STATE_RETURNING, STATE_ERROR}, STATE_OFF
+ )
diff --git a/homeassistant/components/vacuum/translations/et.json b/homeassistant/components/vacuum/translations/et.json
index 56976340c5b8fd..fbdbe330b83d4f 100644
--- a/homeassistant/components/vacuum/translations/et.json
+++ b/homeassistant/components/vacuum/translations/et.json
@@ -1,4 +1,18 @@
{
+ "device_automation": {
+ "action_type": {
+ "clean": "{entity_name} puhastamise lubamine",
+ "dock": "Laske {entity_name} dokki naasta"
+ },
+ "condition_type": {
+ "is_cleaning": "{entity_name} puhastab",
+ "is_docked": "{entity_name} on emajaamas"
+ },
+ "trigger_type": {
+ "cleaning": "{entity_name} alustas puhastamist",
+ "docked": "{entity_name} on emajaamas"
+ }
+ },
"state": {
"_": {
"cleaning": "Puhastamine",
@@ -7,9 +21,9 @@
"idle": "Ootel",
"off": "V\u00e4ljas",
"on": "Sees",
- "paused": "Peatatud",
- "returning": "P\u00f6\u00f6rdun tagasi dokki"
+ "paused": "Pausil",
+ "returning": "P\u00f6\u00f6rdun tagasi laadimisjaama"
}
},
- "title": "T\u00fchjenda"
+ "title": "Tolmuimeja"
}
\ No newline at end of file
diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json
index 97e3955792c2d8..7a959654525815 100644
--- a/homeassistant/components/vallox/manifest.json
+++ b/homeassistant/components/vallox/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "vallox",
- "name": "Valloxs",
+ "name": "Vallox",
"documentation": "https://www.home-assistant.io/integrations/vallox",
"requirements": ["vallox-websocket-api==2.4.0"],
"codeowners": []
diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py
index 361b5a01175788..7d392ab37c5aa4 100644
--- a/homeassistant/components/velbus/config_flow.py
+++ b/homeassistant/components/velbus/config_flow.py
@@ -37,7 +37,7 @@ def _test_connection(self, prt):
try:
controller = velbus.Controller(prt)
except Exception: # pylint: disable=broad-except
- self._errors[CONF_PORT] = "connection_failed"
+ self._errors[CONF_PORT] = "cannot_connect"
return False
controller.stop()
return True
@@ -58,7 +58,7 @@ async def async_step_user(self, user_input=None):
if self._test_connection(prt):
return self._create_device(name, prt)
else:
- self._errors[CONF_PORT] = "port_exists"
+ self._errors[CONF_PORT] = "already_configured"
else:
user_input = {}
user_input[CONF_NAME] = ""
@@ -82,5 +82,5 @@ async def async_step_import(self, user_input=None):
if self._prt_in_configuration_exists(prt):
# if the velbus import is already in the config
# we should not proceed the import
- return self.async_abort(reason="port_exists")
+ return self.async_abort(reason="already_configured")
return await self.async_step_user(user_input)
diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json
index 455aa98b34c204..368c4865bab339 100644
--- a/homeassistant/components/velbus/manifest.json
+++ b/homeassistant/components/velbus/manifest.json
@@ -2,7 +2,7 @@
"domain": "velbus",
"name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus",
- "requirements": ["python-velbus==2.0.44"],
+ "requirements": ["python-velbus==2.0.46"],
"config_flow": true,
"codeowners": ["@Cereal2nd", "@brefra"]
}
diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json
index d5f9d4e7ccfc05..c2defd782f4418 100644
--- a/homeassistant/components/velbus/strings.json
+++ b/homeassistant/components/velbus/strings.json
@@ -10,9 +10,11 @@
}
},
"error": {
- "port_exists": "This port is already configured",
- "connection_failed": "The velbus connection failed"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
- "abort": { "port_exists": "This port is already configured" }
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
}
}
diff --git a/homeassistant/components/velbus/translations/ca.json b/homeassistant/components/velbus/translations/ca.json
index 783bb6d7f3b193..b6680ace1d645f 100644
--- a/homeassistant/components/velbus/translations/ca.json
+++ b/homeassistant/components/velbus/translations/ca.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"port_exists": "El port ja est\u00e0 configurat"
},
"error": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_failed": "Ha fallat la connexi\u00f3 Velbus",
"port_exists": "El port ja est\u00e0 configurat"
},
diff --git a/homeassistant/components/velbus/translations/el.json b/homeassistant/components/velbus/translations/el.json
new file mode 100644
index 00000000000000..04b238a916d221
--- /dev/null
+++ b/homeassistant/components/velbus/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/translations/en.json b/homeassistant/components/velbus/translations/en.json
index ab455442891157..f0b6d16c7bec73 100644
--- a/homeassistant/components/velbus/translations/en.json
+++ b/homeassistant/components/velbus/translations/en.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "Device is already configured",
"port_exists": "This port is already configured"
},
"error": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
"connection_failed": "The velbus connection failed",
"port_exists": "This port is already configured"
},
diff --git a/homeassistant/components/velbus/translations/es.json b/homeassistant/components/velbus/translations/es.json
index 1bde7176eaf3b8..0c29e2d850506f 100644
--- a/homeassistant/components/velbus/translations/es.json
+++ b/homeassistant/components/velbus/translations/es.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
"port_exists": "Este puerto ya est\u00e1 configurado"
},
"error": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar",
"connection_failed": "La conexi\u00f3n velbus fall\u00f3",
"port_exists": "Este puerto ya est\u00e1 configurado"
},
diff --git a/homeassistant/components/velbus/translations/et.json b/homeassistant/components/velbus/translations/et.json
new file mode 100644
index 00000000000000..cb3316a844f65f
--- /dev/null
+++ b/homeassistant/components/velbus/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "port_exists": "See port on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendus nurjus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/translations/fr.json b/homeassistant/components/velbus/translations/fr.json
index ab2e8d756e6696..1c771b9d7a2cd1 100644
--- a/homeassistant/components/velbus/translations/fr.json
+++ b/homeassistant/components/velbus/translations/fr.json
@@ -1,9 +1,11 @@
{
"config": {
"abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"connection_failed": "La connexion velbus a \u00e9chou\u00e9",
"port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9"
},
diff --git a/homeassistant/components/velbus/translations/it.json b/homeassistant/components/velbus/translations/it.json
index bf24ff11336dd6..3ee9a0c02deced 100644
--- a/homeassistant/components/velbus/translations/it.json
+++ b/homeassistant/components/velbus/translations/it.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"port_exists": "Questa porta \u00e8 gi\u00e0 configurata"
},
"error": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
"connection_failed": "La connessione Velbus non \u00e8 riuscita",
"port_exists": "Questa porta \u00e8 gi\u00e0 configurata"
},
diff --git a/homeassistant/components/velbus/translations/lb.json b/homeassistant/components/velbus/translations/lb.json
index 5bb18bc5fa495c..a791e4ce0462d8 100644
--- a/homeassistant/components/velbus/translations/lb.json
+++ b/homeassistant/components/velbus/translations/lb.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
"port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert"
},
"error": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "cannot_connect": "Feeler beim verbannen",
"connection_failed": "Feeler bei der velbus Verbindung",
"port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert"
},
diff --git a/homeassistant/components/velbus/translations/no.json b/homeassistant/components/velbus/translations/no.json
index 0cc2f475820c89..99732e6dd9a83b 100644
--- a/homeassistant/components/velbus/translations/no.json
+++ b/homeassistant/components/velbus/translations/no.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "Enheten er allerede konfigurert",
"port_exists": "Denne porten er allerede konfigurert"
},
"error": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_failed": "Velbus-tilkoblingen mislyktes",
"port_exists": "Denne porten er allerede konfigurert"
},
diff --git a/homeassistant/components/velbus/translations/pl.json b/homeassistant/components/velbus/translations/pl.json
index c3f06b312f4580..40717086153342 100644
--- a/homeassistant/components/velbus/translations/pl.json
+++ b/homeassistant/components/velbus/translations/pl.json
@@ -4,6 +4,7 @@
"port_exists": "Ten port jest ju\u017c skonfigurowany."
},
"error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119",
"port_exists": "Ten port jest ju\u017c skonfigurowany."
},
diff --git a/homeassistant/components/velbus/translations/ru.json b/homeassistant/components/velbus/translations/ru.json
index e88f6209eeeb0d..7a80b18f03e959 100644
--- a/homeassistant/components/velbus/translations/ru.json
+++ b/homeassistant/components/velbus/translations/ru.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d."
},
"error": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 Velbus.",
"port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d."
},
diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json
index 48f9ef5919b9d7..6a9aeec0bd0241 100644
--- a/homeassistant/components/velbus/translations/zh-Hant.json
+++ b/homeassistant/components/velbus/translations/zh-Hant.json
@@ -1,9 +1,12 @@
{
"config": {
"abort": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_failed": "Velbus \u9023\u7dda\u5931\u6557",
"port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json
index 73306bca7b5d68..0fdbfb649997f6 100644
--- a/homeassistant/components/velux/manifest.json
+++ b/homeassistant/components/velux/manifest.json
@@ -2,6 +2,6 @@
"domain": "velux",
"name": "Velux",
"documentation": "https://www.home-assistant.io/integrations/velux",
- "requirements": ["pyvlx==0.2.16"],
+ "requirements": ["pyvlx==0.2.17"],
"codeowners": ["@Julius2342"]
}
diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py
index b45716b33d6ff1..fdc8503ed70b12 100644
--- a/homeassistant/components/vera/__init__.py
+++ b/homeassistant/components/vera/__init__.py
@@ -2,6 +2,7 @@
import asyncio
from collections import defaultdict
import logging
+from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
import pyvera as veraApi
from requests.exceptions import RequestException
@@ -19,17 +20,25 @@
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import convert, slugify
from homeassistant.util.dt import utc_from_timestamp
-from .common import ControllerData, SubscriptionRegistry, get_configured_platforms
+from .common import (
+ ControllerData,
+ SubscriptionRegistry,
+ get_configured_platforms,
+ get_controller_data,
+ 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,
)
@@ -54,6 +63,8 @@
async def async_setup(hass: HomeAssistant, base_config: dict) -> bool:
"""Set up for Vera controllers."""
+ hass.data[DOMAIN] = {}
+
config = base_config.get(DOMAIN)
if not config:
@@ -107,10 +118,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
all_devices = await hass.async_add_executor_job(controller.get_devices)
all_scenes = await hass.async_add_executor_job(controller.get_scenes)
- except RequestException:
+ except RequestException as exception:
# There was a network related error connecting to the Vera controller.
_LOGGER.exception("Error communicating with Vera API")
- return False
+ raise ConfigEntryNotReady from exception
# Exclude devices unwanted by user.
devices = [device for device in all_devices if device.device_id not in exclude_ids]
@@ -118,20 +129,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
vera_devices = defaultdict(list)
for device in devices:
device_type = map_vera_device(device, light_ids)
- if device_type is None:
- continue
-
- vera_devices[device_type].append(device)
+ if device_type is not None:
+ vera_devices[device_type].append(device)
vera_scenes = []
for scene in all_scenes:
vera_scenes.append(scene)
controller_data = ControllerData(
- controller=controller, devices=vera_devices, scenes=vera_scenes
+ controller=controller,
+ devices=vera_devices,
+ scenes=vera_scenes,
+ config_entry=config_entry,
)
- hass.data[DOMAIN] = controller_data
+ set_controller_data(hass, config_entry, controller_data)
# Forward the config data to the necessary platforms.
for platform in get_configured_platforms(controller_data):
@@ -144,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Withings config entry."""
- controller_data: ControllerData = hass.data[DOMAIN]
+ controller_data: ControllerData = get_controller_data(hass, config_entry)
tasks = [
hass.config_entries.async_forward_entry_unload(config_entry, platform)
@@ -156,66 +168,78 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return True
-def map_vera_device(vera_device, remap):
+def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str:
"""Map vera classes to Home Assistant types."""
- if isinstance(vera_device, veraApi.VeraDimmer):
- return "light"
- if isinstance(vera_device, veraApi.VeraBinarySensor):
- return "binary_sensor"
- if isinstance(vera_device, veraApi.VeraSensor):
- return "sensor"
- if isinstance(vera_device, veraApi.VeraArmableDevice):
- return "switch"
- if isinstance(vera_device, veraApi.VeraLock):
- return "lock"
- if isinstance(vera_device, veraApi.VeraThermostat):
- return "climate"
- if isinstance(vera_device, veraApi.VeraCurtain):
- return "cover"
- if isinstance(vera_device, veraApi.VeraSceneController):
- return "sensor"
- if isinstance(vera_device, veraApi.VeraSwitch):
- if vera_device.device_id in remap:
+ type_map = {
+ veraApi.VeraDimmer: "light",
+ veraApi.VeraBinarySensor: "binary_sensor",
+ veraApi.VeraSensor: "sensor",
+ veraApi.VeraArmableDevice: "switch",
+ veraApi.VeraLock: "lock",
+ veraApi.VeraThermostat: "climate",
+ veraApi.VeraCurtain: "cover",
+ veraApi.VeraSceneController: "sensor",
+ veraApi.VeraSwitch: "switch",
+ }
+
+ def map_special_case(instance_class: Type, entity_type: str) -> str:
+ if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap:
return "light"
- return "switch"
- return None
+ return entity_type
+
+ return next(
+ iter(
+ map_special_case(instance_class, entity_type)
+ for instance_class, entity_type in type_map.items()
+ if isinstance(vera_device, instance_class)
+ ),
+ None,
+ )
-class VeraDevice(Entity):
+DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice)
+
+
+class VeraDevice(Generic[DeviceType], Entity):
"""Representation of a Vera device entity."""
- def __init__(self, vera_device, controller):
+ def __init__(self, vera_device: DeviceType, controller_data: ControllerData):
"""Initialize the device."""
self.vera_device = vera_device
- self.controller = controller
+ self.controller = controller_data.controller
self._name = self.vera_device.name
# Append device id to prevent name clashes in HA.
self.vera_id = VERA_ID_FORMAT.format(
- slugify(vera_device.name), vera_device.device_id
+ slugify(vera_device.name), vera_device.vera_device_id
)
- async def async_added_to_hass(self):
+ if controller_data.config_entry.data.get(CONF_LEGACY_UNIQUE_ID):
+ self._unique_id = str(self.vera_device.vera_device_id)
+ else:
+ self._unique_id = f"vera_{controller_data.config_entry.unique_id}_{self.vera_device.vera_device_id}"
+
+ async def async_added_to_hass(self) -> None:
"""Subscribe to updates."""
self.controller.register(self.vera_device, self._update_callback)
- def _update_callback(self, _device):
+ def _update_callback(self, _device: DeviceType) -> None:
"""Update the state."""
self.schedule_update_ha_state(True)
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the device."""
return self._name
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""Get polling requirement from vera device."""
return self.vera_device.should_poll
@property
- def device_state_attributes(self):
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes of the device."""
attr = {}
@@ -254,4 +278,4 @@ def unique_id(self) -> str:
The Vera assigns a unique and immutable ID number to each device.
"""
- return str(self.vera_device.vera_device_id)
+ return self._unique_id
diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py
index 557874f846a4bb..2e66d38e2492ac 100644
--- a/homeassistant/components/vera/binary_sensor.py
+++ b/homeassistant/components/vera/binary_sensor.py
@@ -1,6 +1,8 @@
"""Support for Vera binary sensors."""
import logging
-from typing import Callable, List
+from typing import Callable, List, Optional
+
+import pyvera as veraApi
from homeassistant.components.binary_sensor import (
DOMAIN as PLATFORM_DOMAIN,
@@ -12,7 +14,7 @@
from homeassistant.helpers.entity import Entity
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -23,29 +25,31 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraBinarySensor(device, controller_data.controller)
+ VeraBinarySensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraBinarySensor(VeraDevice, BinarySensorEntity):
+class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity):
"""Representation of a Vera Binary Sensor."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraBinarySensor, controller_data: ControllerData
+ ):
"""Initialize the binary_sensor."""
self._state = False
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def is_on(self):
+ def is_on(self) -> Optional[bool]:
"""Return true if sensor is on."""
return self._state
- def update(self):
+ def update(self) -> None:
"""Get the latest data and update the state."""
self._state = self.vera_device.is_tripped
diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py
index 9b8601e45d193b..0946de4a37961d 100644
--- a/homeassistant/components/vera/climate.py
+++ b/homeassistant/components/vera/climate.py
@@ -1,6 +1,8 @@
"""Support for Vera thermostats."""
import logging
-from typing import Callable, List
+from typing import Any, Callable, List, Optional
+
+import pyvera as veraApi
from homeassistant.components.climate import (
DOMAIN as PLATFORM_DOMAIN,
@@ -24,7 +26,7 @@
from homeassistant.util import convert
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -40,30 +42,32 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraThermostat(device, controller_data.controller)
+ VeraThermostat(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraThermostat(VeraDevice, ClimateEntity):
+class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity):
"""Representation of a Vera Thermostat."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData
+ ):
"""Initialize the Vera device."""
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def supported_features(self):
+ def supported_features(self) -> Optional[int]:
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
- def hvac_mode(self):
+ def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVAC_MODE_*.
@@ -78,7 +82,7 @@ def hvac_mode(self):
return HVAC_MODE_OFF
@property
- def hvac_modes(self):
+ def hvac_modes(self) -> List[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
@@ -86,7 +90,7 @@ def hvac_modes(self):
return SUPPORT_HVAC
@property
- def fan_mode(self):
+ def fan_mode(self) -> Optional[str]:
"""Return the fan setting."""
mode = self.vera_device.get_fan_mode()
if mode == "ContinuousOn":
@@ -94,11 +98,11 @@ def fan_mode(self):
return FAN_AUTO
@property
- def fan_modes(self):
+ def fan_modes(self) -> Optional[List[str]]:
"""Return a list of available fan modes."""
return FAN_OPERATION_LIST
- def set_fan_mode(self, fan_mode):
+ def set_fan_mode(self, fan_mode) -> None:
"""Set new target temperature."""
if fan_mode == FAN_ON:
self.vera_device.fan_on()
@@ -108,14 +112,14 @@ def set_fan_mode(self, fan_mode):
self.schedule_update_ha_state()
@property
- def current_power_w(self):
+ def current_power_w(self) -> Optional[float]:
"""Return the current power usage in W."""
power = self.vera_device.power
if power:
return convert(power, float, 0.0)
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return the unit of measurement."""
vera_temp_units = self.vera_device.vera_controller.temperature_units
@@ -125,28 +129,28 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def current_temperature(self):
+ def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self.vera_device.get_current_temperature()
@property
- def operation(self):
+ def operation(self) -> str:
"""Return current operation ie. heat, cool, idle."""
return self.vera_device.get_hvac_mode()
@property
- def target_temperature(self):
+ def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
return self.vera_device.get_current_goal_temperature()
- def set_temperature(self, **kwargs):
+ def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if kwargs.get(ATTR_TEMPERATURE) is not None:
self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE))
self.schedule_update_ha_state()
- def set_hvac_mode(self, hvac_mode):
+ def set_hvac_mode(self, hvac_mode) -> None:
"""Set new target hvac mode."""
if hvac_mode == HVAC_MODE_OFF:
self.vera_device.turn_off()
diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py
index 17536bcae69d2d..66a2d6879ddf02 100644
--- a/homeassistant/components/vera/common.py
+++ b/homeassistant/components/vera/common.py
@@ -5,9 +5,12 @@
import pyvera as pv
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import call_later
+from .const import DOMAIN
+
_LOGGER = logging.getLogger(__name__)
@@ -17,6 +20,7 @@ class ControllerData(NamedTuple):
controller: pv.VeraController
devices: DefaultDict[str, List[pv.VeraDevice]]
scenes: List[pv.VeraScene]
+ config_entry: ConfigEntry
def get_configured_platforms(controller_data: ControllerData) -> Set[str]:
@@ -31,6 +35,20 @@ def get_configured_platforms(controller_data: ControllerData) -> Set[str]:
return set(platforms)
+def get_controller_data(
+ hass: HomeAssistant, config_entry: ConfigEntry
+) -> ControllerData:
+ """Get controller data from hass data."""
+ return hass.data[DOMAIN][config_entry.entry_id]
+
+
+def set_controller_data(
+ hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData
+) -> None:
+ """Set controller data in hass data."""
+ hass.data[DOMAIN][config_entry.entry_id] = data
+
+
class SubscriptionRegistry(pv.AbstractSubscriptionRegistry):
"""Manages polling for data from vera."""
diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py
index a040e4b96b533f..754d2eca542801 100644
--- a/homeassistant/components/vera/config_flow.py
+++ b/homeassistant/components/vera/config_flow.py
@@ -8,10 +8,16 @@
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
from homeassistant.core import callback
+from homeassistant.helpers.entity_registry import EntityRegistry
-from .const import CONF_CONTROLLER, DOMAIN
+from .const import ( # pylint: disable=unused-import
+ CONF_CONTROLLER,
+ CONF_LEGACY_UNIQUE_ID,
+ DOMAIN,
+)
LIST_REGEX = re.compile("[^0-9]+")
_LOGGER = logging.getLogger(__name__)
@@ -63,11 +69,11 @@ def options_data(user_input: dict) -> dict:
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Options for the component."""
- def __init__(self, config_entry: config_entries.ConfigEntry):
+ def __init__(self, config_entry: ConfigEntry):
"""Init object."""
self.config_entry = config_entry
- async def async_step_init(self, user_input=None):
+ async def async_step_init(self, user_input: dict = None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(
@@ -86,21 +92,19 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(config_entry) -> OptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
"""Get the options flow."""
return OptionsFlowHandler(config_entry)
async def async_step_user(self, user_input: dict = None):
"""Handle user initiated flow."""
- if self.hass.config_entries.async_entries(DOMAIN):
- return self.async_abort(reason="already_configured")
-
if user_input is not None:
return await self.async_step_finish(
{
**user_input,
**options_data(user_input),
**{CONF_SOURCE: config_entries.SOURCE_USER},
+ **{CONF_LEGACY_UNIQUE_ID: False},
}
)
@@ -113,8 +117,29 @@ async def async_step_user(self, user_input: dict = None):
async def async_step_import(self, config: dict):
"""Handle a flow initialized by import."""
+
+ # If there are entities with the legacy unique_id, then this imported config
+ # should also use the legacy unique_id for entity creation.
+ entity_registry: EntityRegistry = (
+ await self.hass.helpers.entity_registry.async_get_registry()
+ )
+ use_legacy_unique_id = (
+ len(
+ [
+ entry
+ for entry in entity_registry.entities.values()
+ if entry.platform == DOMAIN and entry.unique_id.isdigit()
+ ]
+ )
+ > 0
+ )
+
return await self.async_step_finish(
- {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}}
+ {
+ **config,
+ **{CONF_SOURCE: config_entries.SOURCE_IMPORT},
+ **{CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id},
+ }
)
async def async_step_finish(self, config: dict):
diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py
index c4f1d0efa3a0b2..34ac7faa6698d4 100644
--- a/homeassistant/components/vera/const.py
+++ b/homeassistant/components/vera/const.py
@@ -2,6 +2,7 @@
DOMAIN = "vera"
CONF_CONTROLLER = "vera_controller_url"
+CONF_LEGACY_UNIQUE_ID = "legacy_unique_id"
VERA_ID_FORMAT = "{}_{}"
diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py
index a1f536d9cc12b8..49b15e91eb225b 100644
--- a/homeassistant/components/vera/cover.py
+++ b/homeassistant/components/vera/cover.py
@@ -1,6 +1,8 @@
"""Support for Vera cover - curtains, rollershutters etc."""
import logging
-from typing import Callable, List
+from typing import Any, Callable, List
+
+import pyvera as veraApi
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -13,7 +15,7 @@
from homeassistant.helpers.entity import Entity
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -24,25 +26,27 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraCover(device, controller_data.controller)
+ VeraCover(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraCover(VeraDevice, CoverEntity):
+class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity):
"""Representation a Vera Cover."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraCurtain, controller_data: ControllerData
+ ):
"""Initialize the Vera device."""
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def current_cover_position(self):
+ def current_cover_position(self) -> int:
"""
Return current position of cover.
@@ -55,28 +59,28 @@ def current_cover_position(self):
return 100
return position
- def set_cover_position(self, **kwargs):
+ def set_cover_position(self, **kwargs) -> None:
"""Move the cover to a specific position."""
self.vera_device.set_level(kwargs.get(ATTR_POSITION))
self.schedule_update_ha_state()
@property
- def is_closed(self):
+ def is_closed(self) -> bool:
"""Return if the cover is closed."""
if self.current_cover_position is not None:
return self.current_cover_position == 0
- def open_cover(self, **kwargs):
+ def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self.vera_device.open()
self.schedule_update_ha_state()
- def close_cover(self, **kwargs):
+ def close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self.vera_device.close()
self.schedule_update_ha_state()
- def stop_cover(self, **kwargs):
+ def stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self.vera_device.stop()
self.schedule_update_ha_state()
diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py
index 250842f16874b2..47d2d039d2acee 100644
--- a/homeassistant/components/vera/light.py
+++ b/homeassistant/components/vera/light.py
@@ -1,6 +1,8 @@
"""Support for Vera lights."""
import logging
-from typing import Callable, List
+from typing import Any, Callable, List, Optional, Tuple
+
+import pyvera as veraApi
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -17,7 +19,7 @@
import homeassistant.util.color as color_util
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -28,44 +30,46 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraLight(device, controller_data.controller)
+ VeraLight(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraLight(VeraDevice, LightEntity):
+class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity):
"""Representation of a Vera Light, including dimmable."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraDimmer, controller_data: ControllerData
+ ):
"""Initialize the light."""
self._state = False
self._color = None
self._brightness = None
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def brightness(self):
+ def brightness(self) -> Optional[int]:
"""Return the brightness of the light."""
return self._brightness
@property
- def hs_color(self):
+ def hs_color(self) -> Optional[Tuple[float, float]]:
"""Return the color of the light."""
return self._color
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
if self._color:
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
return SUPPORT_BRIGHTNESS
- def turn_on(self, **kwargs):
+ def turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
if ATTR_HS_COLOR in kwargs and self._color:
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
@@ -78,18 +82,18 @@ def turn_on(self, **kwargs):
self._state = True
self.schedule_update_ha_state(True)
- def turn_off(self, **kwargs):
+ def turn_off(self, **kwargs: Any):
"""Turn the light off."""
self.vera_device.switch_off()
self._state = False
self.schedule_update_ha_state()
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if device is on."""
return self._state
- def update(self):
+ def update(self) -> None:
"""Call to update state."""
self._state = self.vera_device.is_switched_on()
if self.vera_device.is_dimmable:
diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py
index f85beb5ba69bc6..46f8c6f189e5fb 100644
--- a/homeassistant/components/vera/lock.py
+++ b/homeassistant/components/vera/lock.py
@@ -1,6 +1,8 @@
"""Support for Vera locks."""
import logging
-from typing import Callable, List
+from typing import Any, Callable, Dict, List, Optional
+
+import pyvera as veraApi
from homeassistant.components.lock import (
DOMAIN as PLATFORM_DOMAIN,
@@ -13,7 +15,7 @@
from homeassistant.helpers.entity import Entity
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -27,41 +29,41 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraLock(device, controller_data.controller)
+ VeraLock(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraLock(VeraDevice, LockEntity):
+class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity):
"""Representation of a Vera lock."""
- def __init__(self, vera_device, controller):
+ def __init__(self, vera_device: veraApi.VeraLock, controller_data: ControllerData):
"""Initialize the Vera device."""
self._state = None
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
- def lock(self, **kwargs):
+ def lock(self, **kwargs: Any) -> None:
"""Lock the device."""
self.vera_device.lock()
self._state = STATE_LOCKED
- def unlock(self, **kwargs):
+ def unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
self.vera_device.unlock()
self._state = STATE_UNLOCKED
@property
- def is_locked(self):
+ def is_locked(self) -> Optional[bool]:
"""Return true if device is on."""
return self._state == STATE_LOCKED
@property
- def device_state_attributes(self):
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Who unlocked the lock and did a low battery alert fire.
Reports on the previous poll cycle.
@@ -78,7 +80,7 @@ def device_state_attributes(self):
return data
@property
- def changed_by(self):
+ def changed_by(self) -> Optional[str]:
"""Who unlocked the lock.
Reports on the previous poll cycle.
@@ -89,7 +91,7 @@ def changed_by(self):
return last_user[0]
return None
- def update(self):
+ def update(self) -> None:
"""Update state by the Vera device callback."""
self._state = (
STATE_LOCKED if self.vera_device.is_locked(True) else STATE_UNLOCKED
diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json
index a6afcce65b315e..b41d289e6b388d 100644
--- a/homeassistant/components/vera/manifest.json
+++ b/homeassistant/components/vera/manifest.json
@@ -3,6 +3,6 @@
"name": "Vera",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vera",
- "requirements": ["pyvera==0.3.9"],
+ "requirements": ["pyvera==0.3.10"],
"codeowners": ["@vangorra"]
}
diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py
index 2f3069f5332211..8bd4473e1c828e 100644
--- a/homeassistant/components/vera/scene.py
+++ b/homeassistant/components/vera/scene.py
@@ -1,6 +1,8 @@
"""Support for Vera scenes."""
import logging
-from typing import Any, Callable, List
+from typing import Any, Callable, Dict, List, Optional
+
+import pyvera as veraApi
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
@@ -8,7 +10,8 @@
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
-from .const import DOMAIN, VERA_ID_FORMAT
+from .common import ControllerData, get_controller_data
+from .const import VERA_ID_FORMAT
_LOGGER = logging.getLogger(__name__)
@@ -19,22 +22,19 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
- [
- VeraScene(device, controller_data.controller)
- for device in controller_data.scenes
- ]
+ [VeraScene(device, controller_data) for device in controller_data.scenes]
)
class VeraScene(Scene):
"""Representation of a Vera scene entity."""
- def __init__(self, vera_scene, controller):
+ def __init__(self, vera_scene: veraApi.VeraScene, controller_data: ControllerData):
"""Initialize the scene."""
self.vera_scene = vera_scene
- self.controller = controller
+ self.controller = controller_data.controller
self._name = self.vera_scene.name
# Append device id to prevent name clashes in HA.
@@ -42,7 +42,7 @@ def __init__(self, vera_scene, controller):
slugify(vera_scene.name), vera_scene.scene_id
)
- def update(self):
+ def update(self) -> None:
"""Update the scene status."""
self.vera_scene.refresh()
@@ -51,11 +51,11 @@ def activate(self, **kwargs: Any) -> None:
self.vera_scene.activate()
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the scene."""
return self._name
@property
- def device_state_attributes(self):
+ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes of the scene."""
return {"vera_scene_id": self.vera_scene.vera_scene_id}
diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py
index 3c4e0974b850e5..9c3dd097a78d2b 100644
--- a/homeassistant/components/vera/sensor.py
+++ b/homeassistant/components/vera/sensor.py
@@ -1,19 +1,19 @@
"""Support for Vera sensors."""
from datetime import timedelta
import logging
-from typing import Callable, List
+from typing import Callable, List, Optional, cast
import pyvera as veraApi
from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.util import convert
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -26,39 +26,41 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraSensor(device, controller_data.controller)
+ VeraSensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraSensor(VeraDevice, Entity):
+class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity):
"""Representation of a Vera Sensor."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraSensor, controller_data: ControllerData
+ ):
"""Initialize the sensor."""
self.current_value = None
self._temperature_units = None
self.last_changed_time = None
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def state(self):
+ def state(self) -> str:
"""Return the name of the sensor."""
return self.current_value
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> Optional[str]:
"""Return the unit of measurement of this entity, if any."""
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
return self._temperature_units
if self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR:
- return "lx"
+ return LIGHT_LUX
if self.vera_device.category == veraApi.CATEGORY_UV_SENSOR:
return "level"
if self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR:
@@ -66,7 +68,7 @@ def unit_of_measurement(self):
if self.vera_device.category == veraApi.CATEGORY_POWER_METER:
return "watts"
- def update(self):
+ def update(self) -> None:
"""Update the state."""
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
@@ -86,8 +88,9 @@ def update(self):
elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR:
self.current_value = self.vera_device.humidity
elif self.vera_device.category == veraApi.CATEGORY_SCENE_CONTROLLER:
- value = self.vera_device.get_last_scene_id(True)
- time = self.vera_device.get_last_scene_time(True)
+ controller = cast(veraApi.VeraSceneController, self.vera_device)
+ value = controller.get_last_scene_id(True)
+ time = controller.get_last_scene_time(True)
if time == self.last_changed_time:
self.current_value = None
else:
diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json
index 7b294eddbb9d30..844d1777f5d752 100644
--- a/homeassistant/components/vera/strings.json
+++ b/homeassistant/components/vera/strings.json
@@ -1,7 +1,6 @@
{
"config": {
"abort": {
- "already_configured": "A controller is already configured.",
"cannot_connect": "Could not connect to controller with url {base_url}"
},
"step": {
diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py
index 0a9a94d63722da..9e8360bf67346b 100644
--- a/homeassistant/components/vera/switch.py
+++ b/homeassistant/components/vera/switch.py
@@ -1,6 +1,8 @@
"""Support for Vera switches."""
import logging
-from typing import Callable, List
+from typing import Any, Callable, List, Optional
+
+import pyvera as veraApi
from homeassistant.components.switch import (
DOMAIN as PLATFORM_DOMAIN,
@@ -13,7 +15,7 @@
from homeassistant.util import convert
from . import VeraDevice
-from .const import DOMAIN
+from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@@ -24,48 +26,50 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
- controller_data = hass.data[DOMAIN]
+ controller_data = get_controller_data(hass, entry)
async_add_entities(
[
- VeraSwitch(device, controller_data.controller)
+ VeraSwitch(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
-class VeraSwitch(VeraDevice, SwitchEntity):
+class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity):
"""Representation of a Vera Switch."""
- def __init__(self, vera_device, controller):
+ def __init__(
+ self, vera_device: veraApi.VeraSwitch, controller_data: ControllerData
+ ):
"""Initialize the Vera device."""
self._state = False
- VeraDevice.__init__(self, vera_device, controller)
+ VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
- def turn_on(self, **kwargs):
+ def turn_on(self, **kwargs: Any) -> None:
"""Turn device on."""
self.vera_device.switch_on()
self._state = True
self.schedule_update_ha_state()
- def turn_off(self, **kwargs):
+ def turn_off(self, **kwargs: Any) -> None:
"""Turn device off."""
self.vera_device.switch_off()
self._state = False
self.schedule_update_ha_state()
@property
- def current_power_w(self):
+ def current_power_w(self) -> Optional[float]:
"""Return the current power usage in W."""
power = self.vera_device.power
if power:
return convert(power, float, 0.0)
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if device is on."""
return self._state
- def update(self):
+ def update(self) -> None:
"""Update device state."""
self._state = self.vera_device.is_switched_on()
diff --git a/homeassistant/components/vera/translations/fr.json b/homeassistant/components/vera/translations/fr.json
index 9cc6d871dd7992..e54613cdb7843e 100644
--- a/homeassistant/components/vera/translations/fr.json
+++ b/homeassistant/components/vera/translations/fr.json
@@ -7,8 +7,11 @@
"step": {
"user": {
"data": {
+ "exclude": "Identifiants d'appareils Vera \u00e0 exclure de Home Assistant.",
+ "lights": "Identifiants des interrupteurs vera \u00e0 traiter comme des lumi\u00e8res dans Home Assistant",
"vera_controller_url": "URL du contr\u00f4leur"
},
+ "description": "Fournissez une URL de contr\u00f4leur Vera ci-dessous. Cela devrait ressembler \u00e0 ceci : http://192.168.1.161:3480.",
"title": "Configurer le contr\u00f4leur Vera"
}
}
@@ -16,6 +19,11 @@
"options": {
"step": {
"init": {
+ "data": {
+ "exclude": "Identifiants d'appareils Vera \u00e0 exclure de Home Assistant.",
+ "lights": "Identifiants des interrupteurs vera \u00e0 traiter comme des lumi\u00e8res dans Home Assistant"
+ },
+ "description": "Consultez la documentation de vera pour plus de d\u00e9tails sur les param\u00e8tres facultatifs: https://www.home-assistant.io/integrations/vera/. Remarque: toute modification ici n\u00e9cessitera un red\u00e9marrage du serveur Home Assistant. Pour effacer les valeurs, entrez un espace.",
"title": "Options du contr\u00f4leur Vera"
}
}
diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py
index 757e299792acef..8cd8b0672cfd15 100644
--- a/homeassistant/components/verisure/__init__.py
+++ b/homeassistant/components/verisure/__init__.py
@@ -12,6 +12,7 @@
CONF_SCAN_INTERVAL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
+ HTTP_SERVICE_UNAVAILABLE,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
@@ -189,7 +190,7 @@ def update_overview(self):
self.overview = self.session.get_overview()
except verisure.ResponseError as ex:
_LOGGER.error("Could not read overview, %s", ex)
- if ex.status_code == 503: # Service unavailable
+ if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable
_LOGGER.info("Trying to log in again")
self.login()
else:
diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json
index ed3158040d5804..7fc1a097d81a05 100644
--- a/homeassistant/components/version/manifest.json
+++ b/homeassistant/components/version/manifest.json
@@ -2,7 +2,7 @@
"domain": "version",
"name": "Version",
"documentation": "https://www.home-assistant.io/integrations/version",
- "requirements": ["pyhaversion==3.3.0"],
- "codeowners": ["@fabaff"],
+ "requirements": ["pyhaversion==3.4.2"],
+ "codeowners": ["@fabaff", "@ludeeus"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py
index 636e564b816394..dcfdc11537672f 100644
--- a/homeassistant/components/version/sensor.py
+++ b/homeassistant/components/version/sensor.py
@@ -35,6 +35,7 @@
"raspberrypi4-64",
"tinker",
"odroid-c2",
+ "odroid-n2",
"odroid-xu",
]
ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"]
diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py
index 8b0e8ae6781852..90db4db14b8676 100644
--- a/homeassistant/components/vesync/config_flow.py
+++ b/homeassistant/components/vesync/config_flow.py
@@ -51,7 +51,7 @@ async def async_step_import(self, import_config):
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if configured_instances(self.hass):
- return self.async_abort(reason="already_setup")
+ return self.async_abort(reason="single_instance_allowed")
if not user_input:
return self._show_form()
@@ -62,7 +62,7 @@ async def async_step_user(self, user_input=None):
manager = VeSync(self._username, self._password)
login = await self.hass.async_add_executor_job(manager.login)
if not login:
- return self._show_form(errors={"base": "invalid_login"})
+ return self._show_form(errors={"base": "invalid_auth"})
return self.async_create_entry(
title=self._username,
diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py
index 7d395d93a74dd5..7cc3f00e1a0269 100644
--- a/homeassistant/components/vesync/fan.py
+++ b/homeassistant/components/vesync/fan.py
@@ -21,8 +21,8 @@
"LV-PUR131S": "fan",
}
-SPEED_AUTO = "auto"
-FAN_SPEEDS = [SPEED_AUTO, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+FAN_MODE_AUTO = "auto"
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -36,7 +36,6 @@ async def async_discover(devices):
hass.data[DOMAIN][VS_DISPATCHERS].append(disp)
_async_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities)
- return True
@callback
@@ -71,8 +70,8 @@ def supported_features(self):
@property
def speed(self):
"""Return the current speed."""
- if self.smartfan.mode == SPEED_AUTO:
- return SPEED_AUTO
+ if self.smartfan.mode == FAN_MODE_AUTO:
+ return None
if self.smartfan.mode == "manual":
current_level = self.smartfan.fan_level
if current_level is not None:
@@ -105,11 +104,8 @@ def set_speed(self, speed):
if not self.smartfan.is_on:
self.smartfan.turn_on()
- if speed is None or speed == SPEED_AUTO:
- self.smartfan.auto_mode()
- else:
- self.smartfan.manual_mode()
- self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed))
+ self.smartfan.manual_mode()
+ self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed))
def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the device on."""
diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json
index a4e786c2a12500..667cb16d12839c 100644
--- a/homeassistant/components/vesync/manifest.json
+++ b/homeassistant/components/vesync/manifest.json
@@ -8,7 +8,7 @@
"@thegardenmonkey"
],
"requirements": [
- "pyvesync==1.1.0"
+ "pyvesync==1.2.0"
],
"config_flow": true
}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json
index 1bdf32e1037525..8359691effef14 100644
--- a/homeassistant/components/vesync/strings.json
+++ b/homeassistant/components/vesync/strings.json
@@ -10,10 +10,10 @@
}
},
"error": {
- "invalid_login": "Invalid username or password"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
- "already_setup": "Only one Vesync instance is allowed"
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py
index 939240349d1af1..0ce4b931def2f1 100644
--- a/homeassistant/components/vesync/switch.py
+++ b/homeassistant/components/vesync/switch.py
@@ -74,13 +74,14 @@ def __init__(self, plug):
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
- attr = {}
- if hasattr(self.smartplug, "weekly_energy_total"):
- attr["voltage"] = self.smartplug.voltage
- attr["weekly_energy_total"] = self.smartplug.weekly_energy_total
- attr["monthly_energy_total"] = self.smartplug.monthly_energy_total
- attr["yearly_energy_total"] = self.smartplug.yearly_energy_total
- return attr
+ if not hasattr(self.smartplug, "weekly_energy_total"):
+ return {}
+ return {
+ "voltage": self.smartplug.voltage,
+ "weekly_energy_total": self.smartplug.weekly_energy_total,
+ "monthly_energy_total": self.smartplug.monthly_energy_total,
+ "yearly_energy_total": self.smartplug.yearly_energy_total,
+ }
@property
def current_power_w(self):
diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json
index 6de59a51f96707..c9b8882c264601 100644
--- a/homeassistant/components/vilfo/strings.json
+++ b/homeassistant/components/vilfo/strings.json
@@ -11,12 +11,12 @@
}
},
"error": {
- "cannot_connect": "Failed to connect. Please check the information you provided and try again.",
- "invalid_auth": "Invalid authentication. Please check the access token and try again.",
- "unknown": "An unexpected error occurred while setting up the integration."
+ "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": "This Vilfo Router is already configured."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/vilfo/translations/ca.json b/homeassistant/components/vilfo/translations/ca.json
index 639167e4cecfa3..827545f5b54a00 100644
--- a/homeassistant/components/vilfo/translations/ca.json
+++ b/homeassistant/components/vilfo/translations/ca.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "L'encaminador Vilfo ja est\u00e0 configurat."
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar. Verifica la informaci\u00f3 proporcionada i torna-ho a provar.",
- "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida. Comprova el token d'acc\u00e9s i torna-ho a provar.",
- "unknown": "S'ha produ\u00eft un error inesperat durant la configuraci\u00f3 de la integraci\u00f3."
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/en.json b/homeassistant/components/vilfo/translations/en.json
index 57815aa039322f..03f9a9bd23a3a4 100644
--- a/homeassistant/components/vilfo/translations/en.json
+++ b/homeassistant/components/vilfo/translations/en.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "This Vilfo Router is already configured."
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "Failed to connect. Please check the information you provided and try again.",
- "invalid_auth": "Invalid authentication. Please check the access token and try again.",
- "unknown": "An unexpected error occurred while setting up the integration."
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json
index 3777492192070a..b6790d98d399b3 100644
--- a/homeassistant/components/vilfo/translations/fr.json
+++ b/homeassistant/components/vilfo/translations/fr.json
@@ -14,6 +14,7 @@
"access_token": "Jeton d'Acc\u00e8s",
"host": "Nom d'h\u00f4te ou adresse IP"
},
+ "description": "Configurez l'int\u00e9gration du routeur Vilfo. Vous avez besoin du nom d'h\u00f4te / IP de votre routeur Vilfo et d'un jeton d'acc\u00e8s API. Pour plus d'informations sur cette int\u00e9gration et comment obtenir ces d\u00e9tails, visitez: https://www.home-assistant.io/integrations/vilfo",
"title": "Connectez-vous au routeur Vilfo"
}
}
diff --git a/homeassistant/components/vilfo/translations/it.json b/homeassistant/components/vilfo/translations/it.json
index 8d41d74c79d80d..ad15381b1db7ad 100644
--- a/homeassistant/components/vilfo/translations/it.json
+++ b/homeassistant/components/vilfo/translations/it.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Questo Vilfo Router \u00e8 gi\u00e0 configurato."
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
- "cannot_connect": "Impossibile connettersi. Controllare le informazioni fornite e riprovare.",
- "invalid_auth": "Autenticazione non valida. Controllare il token di accesso e riprovare.",
- "unknown": "Si \u00e8 verificato un errore imprevisto durante l'impostazione dell'integrazione."
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/no.json b/homeassistant/components/vilfo/translations/no.json
index 18b51391d8575d..ce99f9d81abbec 100644
--- a/homeassistant/components/vilfo/translations/no.json
+++ b/homeassistant/components/vilfo/translations/no.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Denne Vilfo Ruteren er allerede konfigurert."
+ "already_configured": "Enheten er allerede konfigurert"
},
"error": {
- "cannot_connect": "Tilkobling mislyktes. Vennligst sjekk informasjonen du oppga, og pr\u00f8v igjen.",
- "invalid_auth": "Ugyldig godkjenning. Vennligst sjekk access token, og pr\u00f8v p\u00e5 nytt.",
- "unknown": "Det oppstod en uventet feil under installasjonen av integrasjonen."
+ "cannot_connect": "Tilkobling mislyktes.",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/pl.json b/homeassistant/components/vilfo/translations/pl.json
index 29a8a05636136a..fb439da9bfe9a1 100644
--- a/homeassistant/components/vilfo/translations/pl.json
+++ b/homeassistant/components/vilfo/translations/pl.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a wprowadzone dane i spr\u00f3buj ponownie.",
"invalid_auth": "Nieudane uwierzytelnienie. Sprawd\u017a token dost\u0119pu i spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/ru.json b/homeassistant/components/vilfo/translations/ru.json
index 329a5e400e5d70..8e61be904004e8 100644
--- a/homeassistant/components/vilfo/translations/ru.json
+++ b/homeassistant/components/vilfo/translations/ru.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "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, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
- "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438."
+ "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": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json
index c1fe87f65e8df4..abbc12e6d8f58a 100644
--- a/homeassistant/components/vilfo/translations/zh-Hant.json
+++ b/homeassistant/components/vilfo/translations/zh-Hant.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Vilfo \u8def\u7531\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u8f38\u5165\u8cc7\u6599\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002",
- "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u5b58\u53d6\u5bc6\u9470\u5f8c\u518d\u8a66\u4e00\u6b21\u3002",
- "unknown": "\u8a2d\u5b9a\u6574\u5408\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py
index 6bf9fdace5a77b..b1c7d2bb2a79f9 100644
--- a/homeassistant/components/vivotek/camera.py
+++ b/homeassistant/components/vivotek/camera.py
@@ -52,9 +52,9 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Vivotek IP Camera."""
creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}"
- args = dict(
- config=config,
- cam=VivotekCamera(
+ args = {
+ "config": config,
+ "cam": VivotekCamera(
host=config[CONF_IP_ADDRESS],
port=(443 if config[CONF_SSL] else 80),
verify_ssl=config[CONF_VERIFY_SSL],
@@ -63,8 +63,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION,
sec_lvl=config[CONF_SECURITY_LEVEL],
),
- stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}",
- )
+ "stream_source": f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}",
+ }
add_entities([VivotekCam(**args)], True)
diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py
index 25960da72cfc9c..a7a9c404f74d9c 100644
--- a/homeassistant/components/vizio/__init__.py
+++ b/homeassistant/components/vizio/__init__.py
@@ -94,7 +94,7 @@ async def async_unload_entry(
and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV
for entry in hass.config_entries.async_entries(DOMAIN)
):
- hass.data[DOMAIN].pop(CONF_APPS)
+ hass.data[DOMAIN].pop(CONF_APPS, None)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py
index ab5386c151b02f..61c9ca54854f8a 100644
--- a/homeassistant/components/vizio/media_player.py
+++ b/homeassistant/components/vizio/media_player.py
@@ -143,7 +143,6 @@ def __init__(
) -> None:
"""Initialize Vizio device."""
self._config_entry = config_entry
- self._async_unsub_listeners = []
self._apps_coordinator = apps_coordinator
self._name = name
@@ -290,7 +289,7 @@ async def _async_send_update_options_signal(
) -> None:
"""Send update event when Vizio config entry is updated."""
# Move this method to component level if another entity ever gets added for a single config entry.
- # See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121
+ # See here: https://github.com/home-assistant/core/pull/30653#discussion_r366426121
async_dispatcher_send(hass, config_entry.entry_id, config_entry)
async def _async_update_options(self, config_entry: ConfigEntry) -> None:
@@ -312,14 +311,14 @@ async def async_update_setting(
async def async_added_to_hass(self) -> None:
"""Register callbacks when entity is added."""
# Register callback for when config entry is updated.
- self._async_unsub_listeners.append(
+ self.async_on_remove(
self._config_entry.add_update_listener(
self._async_send_update_options_signal
)
)
# Register callback for update event
- self._async_unsub_listeners.append(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass, self._config_entry.entry_id, self._async_update_options
)
@@ -333,17 +332,10 @@ def apps_list_update():
self.async_write_ha_state()
if self._device_class == DEVICE_CLASS_TV:
- self._async_unsub_listeners.append(
+ self.async_on_remove(
self._apps_coordinator.async_add_listener(apps_list_update)
)
- async def async_will_remove_from_hass(self) -> None:
- """Disconnect callbacks when entity is removed."""
- for listener in self._async_unsub_listeners:
- listener()
-
- self._async_unsub_listeners.clear()
-
@property
def available(self) -> bool:
"""Return the availabiliity of the device."""
diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json
index 8979f6fd82ea25..da039b6a89e5e5 100644
--- a/homeassistant/components/vizio/strings.json
+++ b/homeassistant/components/vizio/strings.json
@@ -5,7 +5,7 @@
"title": "VIZIO SmartCast Device",
"description": "An [%key:common::config_flow::data::access_token%] is only needed for TVs. If you are configuring a TV and do not have an [%key:common::config_flow::data::access_token%] yet, leave it blank to go through a pairing process.",
"data": {
- "name": "Name",
+ "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
"device_class": "Device Type",
"access_token": "[%key:common::config_flow::data::access_token%]"
@@ -15,7 +15,7 @@
"title": "Complete Pairing Process",
"description": "Your TV should be displaying a code. Enter that code into the form and then continue to the next step to complete the pairing.",
"data": {
- "pin": "PIN"
+ "pin": "[%key:common::config_flow::data::pin%]"
}
},
"pairing_complete": {
@@ -50,4 +50,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json
index 469649275e1153..9c0b31ca4270eb 100644
--- a/homeassistant/components/vizio/translations/hu.json
+++ b/homeassistant/components/vizio/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
"updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt."
},
"error": {
diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json
index c56171e9319244..310fd7650265a7 100644
--- a/homeassistant/components/vizio/translations/ko.json
+++ b/homeassistant/components/vizio/translations/ko.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "existing_config_entry_found": "\uc77c\ub828 \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c \uae30\uc874 VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c\ud569\ub2c8\ub2e4.",
"host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 VIZIO SmartCast \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 VIZIO SmartCast \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json
index 9d22796ea44f2c..ccba8f7de1cf54 100644
--- a/homeassistant/components/vizio/translations/pl.json
+++ b/homeassistant/components/vizio/translations/pl.json
@@ -1,12 +1,13 @@
{
"config": {
"abort": {
- "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"complete_pairing_failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym przes\u0142aniem.",
+ "existing_config_entry_found": "Istnieje ju\u017c wpis konfiguracyjny Konfiguracja klienta Vizio SmartCast z tym samym numerem seryjnym. W celu skonfigurowania tego wpisu nale\u017cy usun\u0105\u0107 istniej\u0105cy.",
"host_exists": "Urz\u0105dzenie Vizio z okre\u015blonym hostem jest ju\u017c skonfigurowane.",
"name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane."
},
diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py
index 23d4c9ff864d93..69029ea7031cd9 100644
--- a/homeassistant/components/voicerss/tts.py
+++ b/homeassistant/components/voicerss/tts.py
@@ -28,32 +28,55 @@
]
SUPPORT_LANGUAGES = [
+ "ar-eg",
+ "ar-sa",
+ "bg-bg",
"ca-es",
"zh-cn",
"zh-hk",
"zh-tw",
+ "hr-hr",
+ "cs-cz",
"da-dk",
+ "nl-be",
"nl-nl",
"en-au",
"en-ca",
"en-gb",
"en-in",
+ "en-ie",
"en-us",
"fi-fi",
"fr-ca",
"fr-fr",
+ "fr-ch",
+ "de-at",
"de-de",
+ "de-ch",
+ "el-gr",
+ "he-il",
+ "hi-in",
+ "hu-hu",
+ "id-id",
"it-it",
"ja-jp",
"ko-kr",
+ "ms-my",
"nb-no",
"pl-pl",
"pt-br",
"pt-pt",
+ "ro-ro",
"ru-ru",
+ "sk-sk",
+ "sl-si",
"es-mx",
"es-es",
"sv-se",
+ "ta-in",
+ "th-th",
+ "tr-tr",
+ "vi-vn",
]
SUPPORT_CODECS = ["mp3", "wav", "aac", "ogg", "caf"]
diff --git a/homeassistant/components/volumio/translations/fr.json b/homeassistant/components/volumio/translations/fr.json
index 03844ccf99b016..6dee3fb9fafd72 100644
--- a/homeassistant/components/volumio/translations/fr.json
+++ b/homeassistant/components/volumio/translations/fr.json
@@ -1,13 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "Impossible de se connecter au Volumio d\u00e9couvert"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
"unknown": "Erreur inattendue"
},
"step": {
+ "discovery_confirm": {
+ "description": "Voulez-vous ajouter Volumio (` {name} `) \u00e0 Home Assistant?",
+ "title": "Volumio d\u00e9couvert"
+ },
"user": {
"data": {
"host": "H\u00f4te",
diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/volumio/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/pl.json b/homeassistant/components/volumio/translations/pl.json
new file mode 100644
index 00000000000000..431788c2c52fc0
--- /dev/null
+++ b/homeassistant/components/volumio/translations/pl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z odnalezionym Volumio"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "discovery_confirm": {
+ "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistant?",
+ "title": "Wykryte Volumio"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/group.py b/homeassistant/components/water_heater/group.py
new file mode 100644
index 00000000000000..f4ec0ecbc2654c
--- /dev/null
+++ b/homeassistant/components/water_heater/group.py
@@ -0,0 +1,34 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.const import STATE_OFF
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ STATE_ECO,
+ STATE_ELECTRIC,
+ STATE_GAS,
+ STATE_HEAT_PUMP,
+ STATE_HIGH_DEMAND,
+ STATE_PERFORMANCE,
+)
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.on_off_states(
+ {
+ STATE_ECO,
+ STATE_ELECTRIC,
+ STATE_PERFORMANCE,
+ STATE_HIGH_DEMAND,
+ STATE_HEAT_PUMP,
+ STATE_GAS,
+ },
+ STATE_OFF,
+ )
diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py
new file mode 100644
index 00000000000000..4741f8a3b548cd
--- /dev/null
+++ b/homeassistant/components/weather/group.py
@@ -0,0 +1,14 @@
+"""Describe group states."""
+
+
+from homeassistant.components.group import GroupIntegrationRegistry
+from homeassistant.core import callback
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@callback
+def async_describe_on_off_states(
+ hass: HomeAssistantType, registry: GroupIntegrationRegistry
+) -> None:
+ """Describe group on off states."""
+ registry.exclude_domain()
diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py
index e53e31856516d9..82950e4f7f8d2f 100644
--- a/homeassistant/components/webostv/media_player.py
+++ b/homeassistant/components/webostv/media_player.py
@@ -305,7 +305,9 @@ def supported_features(self):
"""Flag media player features that are supported."""
supported = SUPPORT_WEBOSTV
- if self._client.sound_output == "external_arc":
+ if (self._client.sound_output == "external_arc") or (
+ self._client.sound_output == "external_speaker"
+ ):
supported = supported | SUPPORT_WEBOSTV_VOLUME
elif self._client.sound_output != "lineout":
supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET
@@ -318,10 +320,9 @@ def supported_features(self):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
- attributes = {}
- if self._client.sound_output is not None and self.state != STATE_OFF:
- attributes[ATTR_SOUND_OUTPUT] = self._client.sound_output
- return attributes
+ if self._client.sound_output is None and self.state == STATE_OFF:
+ return {}
+ return {ATTR_SOUND_OUTPUT: self._client.sound_output}
@cmd
async def async_turn_off(self):
diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml
index 5719e339793646..430916f7c71fb6 100644
--- a/homeassistant/components/webostv/services.yaml
+++ b/homeassistant/components/webostv/services.yaml
@@ -11,7 +11,7 @@ button:
Name of the button to press. Known possible values are
LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT,
MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN,
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
+ PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
example: "LEFT"
command:
diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py
index 036cd690da2639..11d97f58f50642 100644
--- a/homeassistant/components/websocket_api/commands.py
+++ b/homeassistant/components/websocket_api/commands.py
@@ -17,6 +17,7 @@
from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.event import TrackTemplate, async_track_template_result
from homeassistant.helpers.service import async_get_all_descriptions
+from homeassistant.helpers.template import Template
from homeassistant.loader import IntegrationNotFound, async_get_integration
from . import const, decorators, messages
@@ -77,7 +78,7 @@ def forward_events(event):
):
return
- connection.send_message(messages.event_message(msg["id"], event))
+ connection.send_message(messages.cached_event_message(msg["id"], event))
else:
@@ -87,7 +88,7 @@ def forward_events(event):
if event.event_type == EVENT_TIME_CHANGED:
return
- connection.send_message(messages.event_message(msg["id"], event.as_dict()))
+ connection.send_message(messages.cached_event_message(msg["id"], event))
connection.subscriptions[msg["id"]] = hass.bus.async_listen(
event_type, forward_events
@@ -238,36 +239,40 @@ def handle_ping(hass, connection, msg):
connection.send_message(pong_message(msg["id"]))
-@callback
@decorators.websocket_command(
{
vol.Required("type"): "render_template",
- vol.Required("template"): cv.template,
+ vol.Required("template"): str,
vol.Optional("entity_ids"): cv.entity_ids,
vol.Optional("variables"): dict,
+ vol.Optional("timeout"): vol.Coerce(float),
}
)
-def handle_render_template(hass, connection, msg):
+@decorators.async_response
+async def handle_render_template(hass, connection, msg):
"""Handle render_template command."""
- template = msg["template"]
- template.hass = hass
-
+ template_str = msg["template"]
+ template = Template(template_str, hass)
variables = msg.get("variables")
+ timeout = msg.get("timeout")
info = None
+ if timeout and await template.async_render_will_timeout(timeout):
+ connection.send_error(
+ msg["id"],
+ const.ERR_TEMPLATE_ERROR,
+ f"Exceeded maximum execution time of {timeout}s",
+ )
+ return
+
@callback
def _template_listener(event, updates):
nonlocal info
track_template_result = updates.pop()
result = track_template_result.result
if isinstance(result, TemplateError):
- _LOGGER.error(
- "TemplateError('%s') " "while processing template '%s'",
- result,
- track_template_result.template,
- )
-
- result = None
+ connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result))
+ return
connection.send_message(
messages.event_message(
@@ -275,9 +280,16 @@ def _template_listener(event, updates):
)
)
- info = async_track_template_result(
- hass, [TrackTemplate(template, variables)], _template_listener
- )
+ try:
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template, variables)],
+ _template_listener,
+ raise_on_template_error=True,
+ )
+ except TemplateError as ex:
+ connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
+ return
connection.subscriptions[msg["id"]] = info.async_remove
diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py
index f01a2880b9d817..5f2cfb2257dc8a 100644
--- a/homeassistant/components/websocket_api/const.py
+++ b/homeassistant/components/websocket_api/const.py
@@ -29,6 +29,7 @@
ERR_UNKNOWN_ERROR = "unknown_error"
ERR_UNAUTHORIZED = "unauthorized"
ERR_TIMEOUT = "timeout"
+ERR_TEMPLATE_ERROR = "template_error"
TYPE_RESULT = "result"
diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py
index 7c56fcbc606862..b71b19d5181cec 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -11,17 +11,11 @@
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
-from homeassistant.util.json import (
- find_paths_unserializable_data,
- format_unserializable_data,
-)
from .auth import AuthPhase, auth_required_message
from .const import (
CANCELLATION_ERRORS,
DATA_CONNECTIONS,
- ERR_UNKNOWN_ERROR,
- JSON_DUMP,
MAX_PENDING_MSG,
PENDING_MSG_PEAK,
PENDING_MSG_PEAK_TIME,
@@ -30,7 +24,7 @@
URL,
)
from .error import Disconnect
-from .messages import error_message
+from .messages import message_to_json
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
@@ -72,27 +66,10 @@ async def _writer(self):
self._logger.debug("Sending %s", message)
- if isinstance(message, str):
- await self.wsock.send_str(message)
- continue
+ if not isinstance(message, str):
+ message = message_to_json(message)
- try:
- dumped = JSON_DUMP(message)
- except (ValueError, TypeError):
- await self.wsock.send_json(
- error_message(
- message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response"
- )
- )
- self._logger.error(
- "Unable to serialize to JSON. Bad data found at %s",
- format_unserializable_data(
- find_paths_unserializable_data(message, dump=JSON_DUMP)
- ),
- )
- continue
-
- await self.wsock.send_str(dumped)
+ await self.wsock.send_str(message)
# Clean up the peaker checker when we shut down the writer
if self._peak_checker_unsub:
@@ -153,7 +130,7 @@ async def async_handle(self) -> web.WebSocketResponse:
request = self.request
wsock = self.wsock = web.WebSocketResponse(heartbeat=55)
await wsock.prepare(request)
- self._logger.debug("Connected")
+ self._logger.debug("Connected from %s", request.remote)
self._handle_task = asyncio.current_task()
@callback
diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py
index 27d557e8110d33..52e97b60ccf074 100644
--- a/homeassistant/components/websocket_api/messages.py
+++ b/homeassistant/components/websocket_api/messages.py
@@ -1,11 +1,21 @@
"""Message templates for websocket commands."""
+from functools import lru_cache
+import logging
+from typing import Any, Dict
+
import voluptuous as vol
+from homeassistant.core import Event
from homeassistant.helpers import config_validation as cv
+from homeassistant.util.json import (
+ find_paths_unserializable_data,
+ format_unserializable_data,
+)
from . import const
+_LOGGER = logging.getLogger(__name__)
# mypy: allow-untyped-defs
# Minimal requirements of a message
@@ -18,12 +28,12 @@
BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({vol.Required("id"): cv.positive_int})
-def result_message(iden, result=None):
+def result_message(iden: int, result: Any = None) -> Dict:
"""Return a success result message."""
return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result}
-def error_message(iden, code, message):
+def error_message(iden: int, code: str, message: str) -> Dict:
"""Return an error result message."""
return {
"id": iden,
@@ -33,6 +43,37 @@ def error_message(iden, code, message):
}
-def event_message(iden, event):
+def event_message(iden: int, event: Any) -> Dict:
"""Return an event message."""
return {"id": iden, "type": "event", "event": event}
+
+
+@lru_cache(maxsize=128)
+def cached_event_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 message_to_json(event_message(iden, event))
+
+
+def message_to_json(message: Any) -> str:
+ """Serialize a websocket message to json."""
+ try:
+ return const.JSON_DUMP(message)
+ except (ValueError, TypeError):
+ _LOGGER.error(
+ "Unable to serialize to JSON. Bad data found at %s",
+ format_unserializable_data(
+ find_paths_unserializable_data(message, dump=const.JSON_DUMP)
+ ),
+ )
+ return const.JSON_DUMP(
+ error_message(
+ message["id"], const.ERR_UNKNOWN_ERROR, "Invalid JSON in response"
+ )
+ )
diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json
index bc9755414c654a..357d9d9548382d 100644
--- a/homeassistant/components/wemo/manifest.json
+++ b/homeassistant/components/wemo/manifest.json
@@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
- "requirements": ["pywemo==0.4.46"],
+ "requirements": ["pywemo==0.5.0"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."
diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json
index fe8cc10a74c3b6..f7c6329b1af11c 100644
--- a/homeassistant/components/wemo/strings.json
+++ b/homeassistant/components/wemo/strings.json
@@ -6,8 +6,8 @@
}
},
"abort": {
- "single_instance_allowed": "Only a single configuration of Wemo is possible.",
- "no_devices_found": "No Wemo devices found on the network."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}
diff --git a/homeassistant/components/wemo/translations/ca.json b/homeassistant/components/wemo/translations/ca.json
index 007252042cc8c9..1216504eb855f3 100644
--- a/homeassistant/components/wemo/translations/ca.json
+++ b/homeassistant/components/wemo/translations/ca.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No s'han trobat dispositius Wemo a la xarxa.",
- "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de Wemo."
+ "no_devices_found": "No s'han trobat dispositius a la xarxa",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/en.json b/homeassistant/components/wemo/translations/en.json
index 32ef4cc4cf597c..20a9af468ff19a 100644
--- a/homeassistant/components/wemo/translations/en.json
+++ b/homeassistant/components/wemo/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "No Wemo devices found on the network.",
- "single_instance_allowed": "Only a single configuration of Wemo is possible."
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/it.json b/homeassistant/components/wemo/translations/it.json
index 46204dc6057514..691c7ca46b3f61 100644
--- a/homeassistant/components/wemo/translations/it.json
+++ b/homeassistant/components/wemo/translations/it.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nessun dispositivo Wemo trovato in rete.",
- "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di Wemo."
+ "no_devices_found": "Nessun dispositivo trovato sulla rete",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/no.json b/homeassistant/components/wemo/translations/no.json
index 59ff68d5790f53..59958ac048db2c 100644
--- a/homeassistant/components/wemo/translations/no.json
+++ b/homeassistant/components/wemo/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.",
- "single_instance_allowed": "Kun en konfigurasjon av Wemo er mulig."
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/ru.json b/homeassistant/components/wemo/translations/ru.json
index 7123ac1b60157f..60b1efb3dea4c7 100644
--- a/homeassistant/components/wemo/translations/ru.json
+++ b/homeassistant/components/wemo/translations/ru.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Wemo \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "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": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json
index 29167a13480049..0b9966135b158b 100644
--- a/homeassistant/components/wemo/translations/zh-Hant.json
+++ b/homeassistant/components/wemo/translations/zh-Hant.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Wemo \u8a2d\u5099\u3002",
- "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Wemo\u3002"
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wiffi/strings.json b/homeassistant/components/wiffi/strings.json
index e219b2ecae7d00..d4dc66972c768b 100644
--- a/homeassistant/components/wiffi/strings.json
+++ b/homeassistant/components/wiffi/strings.json
@@ -4,7 +4,7 @@
"user": {
"title": "Setup TCP server for WIFFI devices",
"data": {
- "port": "Server Port"
+ "port": "[%key:common::config_flow::data::port%]"
}
}
},
diff --git a/homeassistant/components/wiffi/translations/ca.json b/homeassistant/components/wiffi/translations/ca.json
index 33fc5015ecdf97..6fe2792888c51a 100644
--- a/homeassistant/components/wiffi/translations/ca.json
+++ b/homeassistant/components/wiffi/translations/ca.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "Port del servidor"
+ "port": "Port"
},
"title": "Configuraci\u00f3 del servidor TCP per a dispositius WIFFI"
}
diff --git a/homeassistant/components/wiffi/translations/en.json b/homeassistant/components/wiffi/translations/en.json
index 0ac1868714dd70..046f37de2c749f 100644
--- a/homeassistant/components/wiffi/translations/en.json
+++ b/homeassistant/components/wiffi/translations/en.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "Server Port"
+ "port": "Port"
},
"title": "Setup TCP server for WIFFI devices"
}
diff --git a/homeassistant/components/wiffi/translations/it.json b/homeassistant/components/wiffi/translations/it.json
index 0884f31cbd7e5d..054bcbc986226d 100644
--- a/homeassistant/components/wiffi/translations/it.json
+++ b/homeassistant/components/wiffi/translations/it.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "Porta del server"
+ "port": "Porta"
},
"title": "Configurare il server TCP per i dispositivi WIFFI"
}
diff --git a/homeassistant/components/wiffi/translations/no.json b/homeassistant/components/wiffi/translations/no.json
index 9f745e0e4a80f5..06e8f3449e14c8 100644
--- a/homeassistant/components/wiffi/translations/no.json
+++ b/homeassistant/components/wiffi/translations/no.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "Serverport"
+ "port": "Port"
},
"title": "Sett opp TCP-server for WIFFI-enheter"
}
diff --git a/homeassistant/components/wiffi/translations/pl.json b/homeassistant/components/wiffi/translations/pl.json
index 810bc4fb1ecf58..5af119f71f2430 100644
--- a/homeassistant/components/wiffi/translations/pl.json
+++ b/homeassistant/components/wiffi/translations/pl.json
@@ -9,7 +9,7 @@
"data": {
"port": "Port serwera"
},
- "title": "Konfiguracja serwera TCP dla urz\u0105dze\u0144 WIFFI"
+ "title": "Konfiguracja serwera TCP dla urz\u0105dze\u0144 WiFi"
}
}
},
diff --git a/homeassistant/components/wiffi/translations/ru.json b/homeassistant/components/wiffi/translations/ru.json
index 50d0ec067180fb..f703edbf23b548 100644
--- a/homeassistant/components/wiffi/translations/ru.json
+++ b/homeassistant/components/wiffi/translations/ru.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430"
+ "port": "\u041f\u043e\u0440\u0442"
},
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 WIFFI"
}
diff --git a/homeassistant/components/wiffi/translations/zh-Hant.json b/homeassistant/components/wiffi/translations/zh-Hant.json
index 77b1488025cfab..ae2956cc5e9faa 100644
--- a/homeassistant/components/wiffi/translations/zh-Hant.json
+++ b/homeassistant/components/wiffi/translations/zh-Hant.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "\u4f3a\u670d\u5668\u901a\u8a0a\u57e0"
+ "port": "\u901a\u8a0a\u57e0"
},
"title": "\u8a2d\u5b9a WIFFI \u8a2d\u5099 TCP \u4f3a\u670d\u5668"
}
diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json
new file mode 100644
index 00000000000000..07d00495af7f9d
--- /dev/null
+++ b/homeassistant/components/wilight/translations/de.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "flow_title": "WiLight: {name}",
+ "step": {
+ "confirm": {
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json
new file mode 100644
index 00000000000000..3f5afd4d729ce7
--- /dev/null
+++ b/homeassistant/components/wilight/translations/el.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "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}",
+ "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}",
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/wilight/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json
new file mode 100644
index 00000000000000..677b104c065634
--- /dev/null
+++ b/homeassistant/components/wilight/translations/ko.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "not_wilight_device": "\uc774 \uc7a5\uce58\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "WiLight {name} \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? \n\n \uc9c0\uc6d0 : {components}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json
new file mode 100644
index 00000000000000..c04105e087870b
--- /dev/null
+++ b/homeassistant/components/wilight/translations/nl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "not_supported_device": "Deze WiLight wordt momenteel niet ondersteund",
+ "not_wilight_device": "Dit apparaat is geen WiLight"
+ },
+ "flow_title": "WiLight: {name}",
+ "step": {
+ "confirm": {
+ "description": "Wil je WiLight {name} ? \n\n Het ondersteunt: {components}",
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json
new file mode 100644
index 00000000000000..45c3d1f8990bed
--- /dev/null
+++ b/homeassistant/components/wilight/translations/pl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "not_supported_device": "Ten WiLight nie jest obecnie obs\u0142ugiwany",
+ "not_wilight_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem WiLight"
+ },
+ "flow_title": "WiLight: {name}",
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz skonfigurowa\u0107 WiLight {name}?\n\nObs\u0142uguje: {components}",
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py
index d8967dd064dc89..77ff464a5bf523 100644
--- a/homeassistant/components/wink/binary_sensor.py
+++ b/homeassistant/components/wink/binary_sensor.py
@@ -3,7 +3,16 @@
import pywink
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_OCCUPANCY,
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_SMOKE,
+ DEVICE_CLASS_SOUND,
+ DEVICE_CLASS_VIBRATION,
+ BinarySensorEntity,
+)
from . import DOMAIN, WinkDevice
@@ -12,17 +21,17 @@
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
"brightness": "light",
- "capturing_audio": "sound",
+ "capturing_audio": DEVICE_CLASS_SOUND,
"capturing_video": None,
"co_detected": "gas",
- "liquid_detected": "moisture",
- "loudness": "sound",
- "motion": "motion",
- "noise": "sound",
- "opened": "opening",
- "presence": "occupancy",
- "smoke_detected": "smoke",
- "vibration": "vibration",
+ "liquid_detected": DEVICE_CLASS_MOISTURE,
+ "loudness": DEVICE_CLASS_SOUND,
+ "motion": DEVICE_CLASS_MOTION,
+ "noise": DEVICE_CLASS_SOUND,
+ "opened": DEVICE_CLASS_OPENING,
+ "presence": DEVICE_CLASS_OCCUPANCY,
+ "smoke_detected": DEVICE_CLASS_SMOKE,
+ "vibration": DEVICE_CLASS_VIBRATION,
}
diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py
index 702479c4112c27..83e92c2250b72d 100644
--- a/homeassistant/components/wirelesstag/__init__.py
+++ b/homeassistant/components/wirelesstag/__init__.py
@@ -12,6 +12,7 @@
CONF_PASSWORD,
CONF_USERNAME,
PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
VOLT,
)
import homeassistant.helpers.config_validation as cv
@@ -226,11 +227,6 @@ def __init__(self, api, tag):
self._name = self._tag.name
self._state = None
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def name(self):
"""Return the name of the sensor."""
@@ -281,7 +277,7 @@ def device_state_attributes(self):
return {
ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100),
ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}{VOLT}",
- ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm",
+ ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}{SIGNAL_STRENGTH_DECIBELS_MILLIWATT}",
ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range,
ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}{PERCENTAGE}",
}
diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py
index af81c0e68d3390..067133f331ffd8 100644
--- a/homeassistant/components/withings/common.py
+++ b/homeassistant/components/withings/common.py
@@ -29,6 +29,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_WEBHOOK_ID,
+ HTTP_UNAUTHORIZED,
MASS_KILOGRAMS,
PERCENTAGE,
SPEED_METERS_PER_SECOND,
@@ -54,7 +55,7 @@
_LOGGER = logging.getLogger(const.LOG_NAMESPACE)
NOT_AUTHENTICATED_ERROR = re.compile(
- "^401,.*",
+ f"^{HTTP_UNAUTHORIZED},.*",
re.IGNORECASE,
)
DATA_UPDATED_SIGNAL = "withings_entity_state_updated"
diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json
index c9d2d7ca22ce6d..05f6d15ca1191f 100644
--- a/homeassistant/components/withings/strings.json
+++ b/homeassistant/components/withings/strings.json
@@ -7,7 +7,7 @@
"description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.",
"data": { "profile": "Profile Name" }
},
- "pick_implementation": { "title": "Pick Authentication Method" },
+ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" },
"reauth": {
"title": "Re-authenticate Profile",
"description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data."
diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json
index e59b6e96775466..d8b69013bbddde 100644
--- a/homeassistant/components/withings/translations/es.json
+++ b/homeassistant/components/withings/translations/es.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Configuraci\u00f3n actualizada para el perfil.",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
- "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n."
+ "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.",
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"create_entry": {
"default": "Autenticado correctamente con Withings."
diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json
index 7ddb1049abb428..a51efff72766d2 100644
--- a/homeassistant/components/withings/translations/fr.json
+++ b/homeassistant/components/withings/translations/fr.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Configuration mise \u00e0 jour pour le profil.",
"authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.",
- "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation."
+ "missing_configuration": "L'int\u00e9gration Withings 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} )"
},
"create_entry": {
"default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9."
diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json
index 74dacdd2cac024..a01672f22272a9 100644
--- a/homeassistant/components/withings/translations/ko.json
+++ b/homeassistant/components/withings/translations/ko.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"create_entry": {
"default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
diff --git a/homeassistant/components/withings/translations/lb.json b/homeassistant/components/withings/translations/lb.json
index a5102baf917a2f..e3ff23e392a65c 100644
--- a/homeassistant/components/withings/translations/lb.json
+++ b/homeassistant/components/withings/translations/lb.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Konfiguratioun aktualis\u00e9iert fir de Profil.",
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
- "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun."
+ "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.",
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Withings authentifiz\u00e9iert."
diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json
index 4f382f02a57019..a54333ab8f2cec 100644
--- a/homeassistant/components/withings/translations/nl.json
+++ b/homeassistant/components/withings/translations/nl.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
- "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen."
+ "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
"create_entry": {
"default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel."
diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json
index 2b39f8fceab77b..bdec62a716087c 100644
--- a/homeassistant/components/withings/translations/no.json
+++ b/homeassistant/components/withings/translations/no.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Konfigurasjon oppdatert for profil.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.",
- "missing_configuration": "Withings-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "missing_configuration": "Withings-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, sjekk {docs_url} ] ( {docs_url} )"
},
"create_entry": {
"default": "Vellykket godkjenning med Withings."
diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json
index 3f31c0585f819a..02e6d6f669cc3c 100644
--- a/homeassistant/components/withings/translations/zh-Hant.json
+++ b/homeassistant/components/withings/translations/zh-Hant.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "\u6b64\u500b\u4eba\u8a2d\u7f6e\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002",
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
- "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u8a2d\u5099\u3002"
diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py
index ecf8ca6e1e008b..a3113335fb5dec 100644
--- a/homeassistant/components/wled/config_flow.py
+++ b/homeassistant/components/wled/config_flow.py
@@ -36,7 +36,7 @@ async def async_step_zeroconf(
) -> Dict[str, Any]:
"""Handle zeroconf discovery."""
if user_input is None:
- return self.async_abort(reason="connection_error")
+ return self.async_abort(reason="cannot_connect")
# Hostname is format: wled-livingroom.local.
host = user_input["hostname"].rstrip(".")
@@ -86,8 +86,8 @@ async def _handle_config_flow(
device = await wled.update()
except WLEDConnectionError:
if source == SOURCE_ZEROCONF:
- return self.async_abort(reason="connection_error")
- return self._show_setup_form({"base": "connection_error"})
+ return self.async_abort(reason="cannot_connect")
+ return self._show_setup_form({"base": "cannot_connect"})
user_input[CONF_MAC] = device.info.mac_address
# Check if already configured
diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py
index 6006952a5801f9..f50e17c01cca2b 100644
--- a/homeassistant/components/wled/const.py
+++ b/homeassistant/components/wled/const.py
@@ -26,7 +26,6 @@
# Units of measurement
CURRENT_MA = "mA"
-SIGNAL_DBM = "dBm"
# Services
SERVICE_EFFECT = "effect"
diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py
index 63a253e6efc1e8..06445d03ba947a 100644
--- a/homeassistant/components/wled/sensor.py
+++ b/homeassistant/components/wled/sensor.py
@@ -10,13 +10,14 @@
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TIMESTAMP,
PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import utcnow
from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity
-from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN, SIGNAL_DBM
+from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -192,7 +193,7 @@ def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> Non
icon="mdi:wifi",
key="wifi_rssi",
name=f"{coordinator.data.info.name} Wi-Fi RSSI",
- unit_of_measurement=SIGNAL_DBM,
+ unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
@property
diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json
index 1197517917c862..f60a4bf3563224 100644
--- a/homeassistant/components/wled/strings.json
+++ b/homeassistant/components/wled/strings.json
@@ -14,11 +14,11 @@
}
},
"error": {
- "connection_error": "Failed to connect to WLED device."
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
- "already_configured": "This WLED device is already configured.",
- "connection_error": "Failed to connect to WLED device."
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json
index 72a8fc519b4707..6eee56045b67d5 100644
--- a/homeassistant/components/wled/translations/ca.json
+++ b/homeassistant/components/wled/translations/ca.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Aquest dispositiu WLED ja est\u00e0 configurat.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu WLED."
},
"error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"connection_error": "No s'ha pogut connectar amb el dispositiu WLED."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/el.json b/homeassistant/components/wled/translations/el.json
new file mode 100644
index 00000000000000..58012c1e4e38de
--- /dev/null
+++ b/homeassistant/components/wled/translations/el.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json
index 2576270808d882..aa448672bf2ea9 100644
--- a/homeassistant/components/wled/translations/en.json
+++ b/homeassistant/components/wled/translations/en.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "This WLED device is already configured.",
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to WLED device."
},
"error": {
+ "cannot_connect": "Failed to connect",
"connection_error": "Failed to connect to WLED device."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json
index aab9752d8b6ed9..5f8a721153156f 100644
--- a/homeassistant/components/wled/translations/es.json
+++ b/homeassistant/components/wled/translations/es.json
@@ -2,9 +2,11 @@
"config": {
"abort": {
"already_configured": "Este dispositivo WLED ya est\u00e1 configurado.",
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se ha podido conectar al dispositivo WLED."
},
"error": {
+ "cannot_connect": "No se pudo conectar",
"connection_error": "No se ha podido conectar al dispositivo WLED."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json
new file mode 100644
index 00000000000000..4c5b7f22a9887d
--- /dev/null
+++ b/homeassistant/components/wled/translations/et.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "See WLED seade on juba h\u00e4\u00e4lestatud.",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "WLED-seadmega \u00fchenduse loomine nurjus."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "WLED-seadmega \u00fchenduse loomine nurjus."
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Seadista WLED-i sidumine Home Assistantiga."
+ },
+ "zeroconf_confirm": {
+ "description": "Kas soovid lisada WLED {nimi} Home Assistanti?",
+ "title": "Leitud WLED seade"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json
index 694627b8baf90b..79ab5ab876b06c 100644
--- a/homeassistant/components/wled/translations/fr.json
+++ b/homeassistant/components/wled/translations/fr.json
@@ -2,9 +2,11 @@
"config": {
"abort": {
"already_configured": "Cet appareil WLED est d\u00e9j\u00e0 configur\u00e9.",
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique WLED."
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
"connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique WLED."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json
index 897c04539f03b5..d6760328db5c74 100644
--- a/homeassistant/components/wled/translations/it.json
+++ b/homeassistant/components/wled/translations/it.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Questo dispositivo WLED \u00e8 gi\u00e0 configurato.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo WLED."
},
"error": {
+ "cannot_connect": "Impossibile connettersi",
"connection_error": "Impossibile connettersi al dispositivo WLED."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/lb.json b/homeassistant/components/wled/translations/lb.json
index 1490e62e068b2b..6843b74174fb99 100644
--- a/homeassistant/components/wled/translations/lb.json
+++ b/homeassistant/components/wled/translations/lb.json
@@ -2,9 +2,11 @@
"config": {
"abort": {
"already_configured": "D\u00ebsen WLED Apparat ass scho konfigur\u00e9iert.",
+ "cannot_connect": "Feeler beim verbannen",
"connection_error": "Feeler beim verbannen mam WLED Apparat."
},
"error": {
+ "cannot_connect": "Feeler beim verbannen",
"connection_error": "Feeler beim verbannen mam WLED Apparat."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json
index 794f68256a731c..ebbc0e52cde98d 100644
--- a/homeassistant/components/wled/translations/nl.json
+++ b/homeassistant/components/wled/translations/nl.json
@@ -1,10 +1,23 @@
{
"config": {
+ "abort": {
+ "already_configured": "Dit WLED-apparaat is al geconfigureerd.",
+ "connection_error": "Kan geen verbinding maken met WLED-apparaat."
+ },
+ "error": {
+ "connection_error": "Kan geen verbinding maken met WLED-apparaat."
+ },
+ "flow_title": "WLED: {name}",
"step": {
"user": {
"data": {
"host": "Hostnaam of IP-adres"
- }
+ },
+ "description": "Stel uw WLED-integratie in met Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?",
+ "title": "Ontdekt WLED-apparaat"
}
}
}
diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json
index da372daad11b95..6352c5ccae561f 100644
--- a/homeassistant/components/wled/translations/no.json
+++ b/homeassistant/components/wled/translations/no.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Denne WLED-enheten er allerede konfigurert.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til WLED-enheten."
},
"error": {
+ "cannot_connect": "Tilkobling mislyktes.",
"connection_error": "Kunne ikke koble til WLED-enheten."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json
index 6f68055d385485..7ff1a406320caf 100644
--- a/homeassistant/components/wled/translations/pl.json
+++ b/homeassistant/components/wled/translations/pl.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"flow_title": "WLED: {name}",
"step": {
diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json
index 5d50b75b94f6e2..3700aa6cc308b6 100644
--- a/homeassistant/components/wled/translations/ru.json
+++ b/homeassistant/components/wled/translations/ru.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"error": {
+ "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 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"flow_title": "WLED: {name}",
diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json
index 87490dc4595c60..26d49a34688668 100644
--- a/homeassistant/components/wled/translations/zh-Hant.json
+++ b/homeassistant/components/wled/translations/zh-Hant.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "WLED \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002"
},
"flow_title": "WLED\uff1a{name}",
diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index 9a272c502a0345..611fa7da315031 100644
--- a/homeassistant/components/wolflink/__init__.py
+++ b/homeassistant/components/wolflink/__init__.py
@@ -2,7 +2,7 @@
from datetime import timedelta
import logging
-from httpcore import ConnectError
+from httpcore import ConnectError, ConnectTimeout
from wolf_smartset.token_auth import InvalidAuth
from wolf_smartset.wolf_client import WolfClient
@@ -99,7 +99,7 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int):
try:
fetched_parameters = await client.fetch_parameters(gateway_id, device_id)
return [param for param in fetched_parameters if param.name != "Reglertyp"]
- except ConnectError as exception:
+ except (ConnectError, ConnectTimeout) as exception:
raise UpdateFailed(f"Error communicating with API: {exception}") from exception
except InvalidAuth as exception:
- raise UpdateFailed("Invalid authentication during update.") from exception
+ raise UpdateFailed("Invalid authentication during update") from exception
diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json
index c188c0903695a9..633318f2f62b32 100644
--- a/homeassistant/components/wolflink/manifest.json
+++ b/homeassistant/components/wolflink/manifest.json
@@ -3,6 +3,6 @@
"name": "Wolf SmartSet Service",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wolflink",
- "requirements": ["wolf_smartset==0.1.4"],
+ "requirements": ["wolf_smartset==0.1.6"],
"codeowners": ["@adamkrol93"]
}
diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py
index 97f48e279889a5..1cae006824bee4 100644
--- a/homeassistant/components/wolflink/sensor.py
+++ b/homeassistant/components/wolflink/sensor.py
@@ -11,13 +11,6 @@
Temperature,
)
-from homeassistant.components.wolflink.const import (
- COORDINATOR,
- DEVICE_ID,
- DOMAIN,
- PARAMETERS,
- STATES,
-)
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
@@ -27,6 +20,8 @@
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from .const import COORDINATOR, DEVICE_ID, DOMAIN, PARAMETERS, STATES
+
_LOGGER = logging.getLogger(__name__)
@@ -63,6 +58,7 @@ def __init__(self, coordinator, wolf_object: Parameter, device_id):
super().__init__(coordinator)
self.wolf_object = wolf_object
self.device_id = device_id
+ self._state = None
@property
def name(self):
@@ -71,8 +67,10 @@ def name(self):
@property
def state(self):
- """Return the state."""
- return self.coordinator.data[self.wolf_object.value_id]
+ """Return the state. Wolf Client is returning only changed values so we need to store old value here."""
+ if self.wolf_object.value_id in self.coordinator.data:
+ self._state = self.coordinator.data[self.wolf_object.value_id]
+ return self._state
@property
def device_state_attributes(self):
@@ -151,7 +149,7 @@ def device_class(self):
@property
def state(self):
"""Return the state converting with supported values."""
- state = self.coordinator.data[self.wolf_object.value_id]
+ state = super().state
resolved_state = [
item for item in self.wolf_object.items if item.value == int(state)
]
diff --git a/homeassistant/components/wolflink/strings.sensor.json b/homeassistant/components/wolflink/strings.sensor.json
index 2ce7df6fae5dd6..75c8199a117517 100644
--- a/homeassistant/components/wolflink/strings.sensor.json
+++ b/homeassistant/components/wolflink/strings.sensor.json
@@ -6,7 +6,7 @@
"aus": "Disabled",
"standby": "Standby",
"auto": "Auto",
- "permanent": "Permament",
+ "permanent": "Permanent",
"initialisierung": "Initialization",
"antilegionellenfunktion": "Anti-legionella Function",
"fernschalter_ein": "Remote control enabled",
diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json
new file mode 100644
index 00000000000000..cb7e571d1e6998
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/de.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "device": {
+ "data": {
+ "device_name": "Ger\u00e4t"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/fr.json b/homeassistant/components/wolflink/translations/fr.json
index aa84ec33d8cdad..6e3348c3647559 100644
--- a/homeassistant/components/wolflink/translations/fr.json
+++ b/homeassistant/components/wolflink/translations/fr.json
@@ -9,11 +9,18 @@
"unknown": "Erreur inattendue"
},
"step": {
+ "device": {
+ "data": {
+ "device_name": "Appareil"
+ },
+ "title": "S\u00e9lectionnez l'appareil WOLF"
+ },
"user": {
"data": {
"password": "Mot de passe",
"username": "Nom d'utilisateur"
- }
+ },
+ "title": "Connexion WOLF SmartSet"
}
}
}
diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json
new file mode 100644
index 00000000000000..4d00f0bfc74883
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/nl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/pl.json b/homeassistant/components/wolflink/translations/pl.json
index 483c73aac3d762..d6d42eafb8aa7f 100644
--- a/homeassistant/components/wolflink/translations/pl.json
+++ b/homeassistant/components/wolflink/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
- "invalid_auth": "Niepoprawne uwierzytelnienie.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"unknown": "[%key::common::config_flow::error::unknown%]"
},
"step": {
diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json
new file mode 100644
index 00000000000000..ef60c1c1ae1487
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.de.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "wolflink__state": {
+ "test": "Test",
+ "tpw": "TPW"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.en.json b/homeassistant/components/wolflink/translations/sensor.en.json
index ea60e2339077fd..bd505e845aec22 100644
--- a/homeassistant/components/wolflink/translations/sensor.en.json
+++ b/homeassistant/components/wolflink/translations/sensor.en.json
@@ -50,7 +50,7 @@
"parallelbetrieb": "Parallel mode",
"partymodus": "Party mode",
"perm_cooling": "PermCooling",
- "permanent": "Permament",
+ "permanent": "Permanent",
"permanentbetrieb": "Permanent mode",
"reduzierter_betrieb": "Limited mode",
"rt_abschaltung": "RT shutdown",
diff --git a/homeassistant/components/wolflink/translations/sensor.fr.json b/homeassistant/components/wolflink/translations/sensor.fr.json
new file mode 100644
index 00000000000000..57cd8435f352b0
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.fr.json
@@ -0,0 +1,87 @@
+{
+ "state": {
+ "wolflink__state": {
+ "1_x_warmwasser": "1 x ECS",
+ "abgasklappe": "Amortisseur de gaz de combustion",
+ "absenkbetrieb": "Mode Recul",
+ "absenkstop": "Arr\u00eat de recul",
+ "aktiviert": "Activ\u00e9",
+ "antilegionellenfunktion": "Fonction anti-l\u00e9gionelle",
+ "at_abschaltung": "Arr\u00eat OT",
+ "at_frostschutz": "Protection antigel OT",
+ "aus": "D\u00e9sactiv\u00e9",
+ "auto": "Auto",
+ "auto_off_cool": "AutoOffCool",
+ "auto_on_cool": "AutoOnCool",
+ "automatik_aus": "Arr\u00eat automatique",
+ "automatik_ein": "Mise en marche automatique",
+ "bereit_keine_ladung": "Pr\u00eat, pas de chargement",
+ "betrieb_ohne_brenner": "Travaille sans br\u00fbleur",
+ "cooling": "Refroidissement",
+ "deaktiviert": "Inactif",
+ "dhw_prior": "Priorit\u00e9 ECS",
+ "eco": "\u00c9co",
+ "ein": "Activ\u00e9",
+ "estrichtrocknung": "S\u00e9chage de chape",
+ "externe_deaktivierung": "D\u00e9sactivation externe",
+ "fernschalter_ein": "Contr\u00f4le \u00e0 distance activ\u00e9",
+ "frost_heizkreis": "Gel du circuit de chauffage",
+ "frost_warmwasser": "Gel ECS",
+ "frostschutz": "Protection antigel",
+ "gasdruck": "Pression du gaz",
+ "glt_betrieb": "Mode BMS",
+ "gradienten_uberwachung": "Surveillance de gradient",
+ "heizbetrieb": "Mode chauffage",
+ "heizgerat_mit_speicher": "Chaudi\u00e8re \u00e0 cylindre",
+ "heizung": "En chauffe",
+ "initialisierung": "Initialisation",
+ "kalibration": "\u00c9talonnage",
+ "kalibration_heizbetrieb": "Calibrage du mode de chauffage",
+ "kalibration_kombibetrieb": "\u00c9talonnage du mode Combi",
+ "kalibration_warmwasserbetrieb": "Calibrage ECS",
+ "kaskadenbetrieb": "Fonctionnement en cascade",
+ "kombibetrieb": "Mode Combi",
+ "kombigerat": "Chaudi\u00e8re combi",
+ "kombigerat_mit_solareinbindung": "Chaudi\u00e8re mixte avec int\u00e9gration solaire",
+ "mindest_kombizeit": "Temps combin\u00e9 minimum",
+ "nachlauf_heizkreispumpe": "Pompe du circuit de chauffage en marche",
+ "nachspulen": "Apr\u00e8s rin\u00e7age",
+ "nur_heizgerat": "Chaudi\u00e8re seulement",
+ "parallelbetrieb": "Mode parall\u00e8le",
+ "partymodus": "Mode festif",
+ "perm_cooling": "Refroidissement permanent",
+ "permanent": "Permanent",
+ "permanentbetrieb": "Mode permanent",
+ "reduzierter_betrieb": "Mode limit\u00e9",
+ "rt_abschaltung": "Arr\u00eat RT",
+ "rt_frostschutz": "Protection antigel RT",
+ "ruhekontakt": "Contact de repos",
+ "schornsteinfeger": "Test d'\u00e9missions",
+ "smart_grid": "SmartGrid",
+ "smart_home": "SmartHome",
+ "softstart": "D\u00e9marrage progressif",
+ "solarbetrieb": "Mode solaire",
+ "sparbetrieb": "Mode \u00e9conomie",
+ "sparen": "\u00c9conomie",
+ "spreizung_hoch": "dT trop large",
+ "spreizung_kf": "Spread KF",
+ "stabilisierung": "Stabilisation",
+ "standby": "En veille",
+ "start": "D\u00e9marrer",
+ "storung": "Faute",
+ "taktsperre": "Anti-cycle",
+ "telefonfernschalter": "Commutateur \u00e0 distance t\u00e9l\u00e9phonique",
+ "test": "Test",
+ "tpw": "TPW",
+ "urlaubsmodus": "Mode vacances",
+ "ventilprufung": "Test de valve",
+ "vorspulen": "Rin\u00e7age d'entr\u00e9e",
+ "warmwasser": "ECS",
+ "warmwasser_schnellstart": "D\u00e9marrage rapide ECS",
+ "warmwasserbetrieb": "Mode ECS",
+ "warmwassernachlauf": "ECS en marche",
+ "warmwasservorrang": "Priorit\u00e9 ECS",
+ "zunden": "Allumage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.no.json b/homeassistant/components/wolflink/translations/sensor.no.json
index fcd93f0b01b12c..75aa91bcf9cc58 100644
--- a/homeassistant/components/wolflink/translations/sensor.no.json
+++ b/homeassistant/components/wolflink/translations/sensor.no.json
@@ -50,7 +50,7 @@
"parallelbetrieb": "Parallell modus",
"partymodus": "Festmodus",
"perm_cooling": "PermKj\u00f8ling",
- "permanent": "permament",
+ "permanent": "Permanent",
"permanentbetrieb": "Permanent modus",
"reduzierter_betrieb": "Begrenset modus",
"rt_abschaltung": "RT-avstengning",
diff --git a/homeassistant/components/wolflink/translations/sensor.zh-Hant.json b/homeassistant/components/wolflink/translations/sensor.zh-Hant.json
index c2be9263bcf391..d9c90824742a6f 100644
--- a/homeassistant/components/wolflink/translations/sensor.zh-Hant.json
+++ b/homeassistant/components/wolflink/translations/sensor.zh-Hant.json
@@ -50,7 +50,7 @@
"parallelbetrieb": "\u4e26\u884c\u6a21\u5f0f",
"partymodus": "\u6d3e\u5c0d\u6a21\u5f0f",
"perm_cooling": "PermCooling",
- "permanent": "\u6c38\u4e45",
+ "permanent": "\u56fa\u5b9a",
"permanentbetrieb": "\u6c38\u4e45\u6a21\u5f0f",
"reduzierter_betrieb": "\u9650\u5236\u6a21\u5f0f",
"rt_abschaltung": "RT \u95dc\u6a5f",
diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py
index 2d9c4f5c9c15ed..e1bd79b7ea0efe 100644
--- a/homeassistant/components/wunderground/sensor.py
+++ b/homeassistant/components/wunderground/sensor.py
@@ -23,7 +23,9 @@
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
+ PRESSURE_INHG,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
@@ -391,7 +393,7 @@ def _get_attributes(rest):
"Precipitation 1hr", "precip_1hr_in", "mdi:umbrella", LENGTH_INCHES
),
"precip_1hr_metric": WUCurrentConditionsSensorConfig(
- "Precipitation 1hr", "precip_1hr_metric", "mdi:umbrella", "mm"
+ "Precipitation 1hr", "precip_1hr_metric", "mdi:umbrella", LENGTH_MILLIMETERS
),
"precip_1hr_string": WUCurrentConditionsSensorConfig(
"Precipitation 1hr", "precip_1hr_string", "mdi:umbrella"
@@ -400,13 +402,13 @@ def _get_attributes(rest):
"Precipitation Today", "precip_today_in", "mdi:umbrella", LENGTH_INCHES
),
"precip_today_metric": WUCurrentConditionsSensorConfig(
- "Precipitation Today", "precip_today_metric", "mdi:umbrella", "mm"
+ "Precipitation Today", "precip_today_metric", "mdi:umbrella", LENGTH_MILLIMETERS
),
"precip_today_string": WUCurrentConditionsSensorConfig(
"Precipitation Today", "precip_today_string", "mdi:umbrella"
),
"pressure_in": WUCurrentConditionsSensorConfig(
- "Pressure", "pressure_in", "mdi:gauge", "inHg", device_class="pressure"
+ "Pressure", "pressure_in", "mdi:gauge", PRESSURE_INHG, device_class="pressure"
),
"pressure_mb": WUCurrentConditionsSensorConfig(
"Pressure", "pressure_mb", "mdi:gauge", "mb", device_class="pressure"
@@ -878,16 +880,36 @@ def _get_attributes(rest):
"mdi:weather-windy",
),
"precip_1d_mm": WUDailySimpleForecastSensorConfig(
- "Precipitation Intensity Today", 0, "qpf_allday", "mm", "mm", "mdi:umbrella"
+ "Precipitation Intensity Today",
+ 0,
+ "qpf_allday",
+ LENGTH_MILLIMETERS,
+ LENGTH_MILLIMETERS,
+ "mdi:umbrella",
),
"precip_2d_mm": WUDailySimpleForecastSensorConfig(
- "Precipitation Intensity Tomorrow", 1, "qpf_allday", "mm", "mm", "mdi:umbrella"
+ "Precipitation Intensity Tomorrow",
+ 1,
+ "qpf_allday",
+ LENGTH_MILLIMETERS,
+ LENGTH_MILLIMETERS,
+ "mdi:umbrella",
),
"precip_3d_mm": WUDailySimpleForecastSensorConfig(
- "Precipitation Intensity in 3 Days", 2, "qpf_allday", "mm", "mm", "mdi:umbrella"
+ "Precipitation Intensity in 3 Days",
+ 2,
+ "qpf_allday",
+ LENGTH_MILLIMETERS,
+ LENGTH_MILLIMETERS,
+ "mdi:umbrella",
),
"precip_4d_mm": WUDailySimpleForecastSensorConfig(
- "Precipitation Intensity in 4 Days", 3, "qpf_allday", "mm", "mm", "mdi:umbrella"
+ "Precipitation Intensity in 4 Days",
+ 3,
+ "qpf_allday",
+ LENGTH_MILLIMETERS,
+ LENGTH_MILLIMETERS,
+ "mdi:umbrella",
),
"precip_1d_in": WUDailySimpleForecastSensorConfig(
"Precipitation Intensity Today",
diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py
index 1f46267967a97a..300fcbfb095e63 100644
--- a/homeassistant/components/xbox_live/sensor.py
+++ b/homeassistant/components/xbox_live/sensor.py
@@ -106,9 +106,7 @@ def state(self):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attributes = {}
- attributes["gamerscore"] = self._gamerscore
- attributes["tier"] = self._tier
+ attributes = {"gamerscore": self._gamerscore, "tier": self._tier}
for device in self._presence:
for title in device["titles"]:
diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py
index fecdfd91a754f7..8fbecee46e9d70 100644
--- a/homeassistant/components/xiaomi_aqara/binary_sensor.py
+++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py
@@ -1,7 +1,11 @@
"""Support for Xiaomi aqara binary sensors."""
import logging
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_OPENING,
+ BinarySensorEntity,
+)
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
@@ -250,7 +254,7 @@ def parse_data(self, data, raw_data):
_LOGGER.debug(
"Skipping heartbeat of the motion sensor. "
"It can introduce an incorrect state because of a firmware "
- "bug (https://github.com/home-assistant/home-assistant/pull/"
+ "bug (https://github.com/home-assistant/core/pull/"
"11631#issuecomment-357507744)"
)
return
@@ -299,7 +303,7 @@ def __init__(self, device, xiaomi_hub, config_entry):
"Door Window Sensor",
xiaomi_hub,
data_key,
- "opening",
+ DEVICE_CLASS_OPENING,
config_entry,
)
@@ -349,7 +353,7 @@ def __init__(self, device, xiaomi_hub, config_entry):
"Water Leak Sensor",
xiaomi_hub,
data_key,
- "moisture",
+ DEVICE_CLASS_MOISTURE,
config_entry,
)
diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py
index fcfa3939c2c9bb..1cc3b2d4633e95 100644
--- a/homeassistant/components/xiaomi_aqara/const.py
+++ b/homeassistant/components/xiaomi_aqara/const.py
@@ -36,10 +36,12 @@
"sensor_86sw1",
"sensor_86sw1.aq1",
"remote.b186acn01",
+ "remote.b186acn02",
"86sw2",
"sensor_86sw2",
"sensor_86sw2.aq1",
"remote.b286acn01",
+ "remote.b286acn02",
"cube",
"sensor_cube",
"sensor_cube.aqgl01",
diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json
index 1a00fc3afd228d..4b6cd76985dda5 100644
--- a/homeassistant/components/xiaomi_aqara/manifest.json
+++ b/homeassistant/components/xiaomi_aqara/manifest.json
@@ -3,7 +3,7 @@
"name": "Xiaomi Gateway (Aqara)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
- "requirements": ["PyXiaomiGateway==0.13.2"],
+ "requirements": ["PyXiaomiGateway==0.13.3"],
"after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"],
"zeroconf": ["_miio._udp.local."]
diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py
index 4a6c7ac14fd80c..5b1d3467d25b63 100644
--- a/homeassistant/components/xiaomi_aqara/sensor.py
+++ b/homeassistant/components/xiaomi_aqara/sensor.py
@@ -9,8 +9,10 @@
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
TEMP_CELSIUS,
)
@@ -23,8 +25,8 @@
"temperature": [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
"humidity": [PERCENTAGE, None, DEVICE_CLASS_HUMIDITY],
"illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE],
- "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE],
- "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE],
+ "lux": [LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE],
+ "pressure": [PRESSURE_HPA, None, DEVICE_CLASS_PRESSURE],
"bed_activity": ["μm", None, None],
"load_power": [POWER_WATT, None, DEVICE_CLASS_POWER],
}
@@ -86,8 +88,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
_LOGGER.warning("Unmapped Device Model")
# Set up battery sensors
+ seen_sids = set() # Set of device sids that are already seen
for devices in gateway.devices.values():
for device in devices:
+ if device["sid"] in seen_sids:
+ continue
+ seen_sids.add(device["sid"])
if device["model"] in BATTERY_MODELS:
entities.append(
XiaomiBatterySensor(device, "Battery", gateway, config_entry)
diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json
index bc50c4918664b5..65711b02f2a2ba 100644
--- a/homeassistant/components/xiaomi_aqara/strings.json
+++ b/homeassistant/components/xiaomi_aqara/strings.json
@@ -36,7 +36,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "Config flow for this gateway is already in progress",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json
index 22d3bbaccd3833..f513f36d1814af 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ca.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "already_in_progress": "El flux de configuraci\u00f3 de la passarel\u00b7la ja est\u00e0 en curs.",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"not_xiaomi_aqara": "No \u00e9s una passarel\u00b7la Xiaomi Aqara, el dispositiu descobert no coincideix amb cap passarel\u00b7la coneguda"
},
"error": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json
new file mode 100644
index 00000000000000..75aa3d537e80ca
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/de.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "flow_title": "Xiaomi Aqara Gateway: {name}",
+ "step": {
+ "user": {
+ "title": "Xiaomi Aqara Gateway"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json
index 4cdc7ee497a9fd..6cbbea96220ad3 100644
--- a/homeassistant/components/xiaomi_aqara/translations/en.json
+++ b/homeassistant/components/xiaomi_aqara/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "Config flow for this gateway is already in progress",
+ "already_in_progress": "Configuration flow is already in progress",
"not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways"
},
"error": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json
index a46dc7563903ca..c5e03cc5c14780 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",
+ "invalid_host": "Adresse IP non valide, voir 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/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json
index dfffec72272d3d..1e5792aad3e623 100644
--- a/homeassistant/components/xiaomi_aqara/translations/it.json
+++ b/homeassistant/components/xiaomi_aqara/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per questo gateway \u00e8 gi\u00e0 in corso",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"not_xiaomi_aqara": "Non \u00e8 un Gateway Xiaomi Aqara, il dispositivo scoperto non corrisponde ai gateway noti"
},
"error": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json
index 90a22ace2b876e..f222ac58babfa7 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ko.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ko.json
@@ -30,7 +30,9 @@
},
"user": {
"data": {
- "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4"
+ "host": "IP \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)",
+ "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4",
+ "mac": "Mac \uc8fc\uc18c(\uc120\ud0dd \uc0ac\ud56d)"
},
"description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \ubc0f Mac \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4",
"title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774"
diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json
index 39f472299d7dff..184670ef7a3fa6 100644
--- a/homeassistant/components/xiaomi_aqara/translations/no.json
+++ b/homeassistant/components/xiaomi_aqara/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for denne porten p\u00e5g\u00e5r allerede",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"not_xiaomi_aqara": "Ikke en Xiaomi Aqara Gateway, oppdaget enhet ikke samsvarer med kjente gatewayer"
},
"error": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json
index a603566d569c72..fba37ad0249b21 100644
--- a/homeassistant/components/xiaomi_aqara/translations/pl.json
+++ b/homeassistant/components/xiaomi_aqara/translations/pl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
- "already_in_progress": "Konfiguracja dla tej bramki jest ju\u017c w toku.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfiguracja dla tej bramki jest ju\u017c w toku",
"not_xiaomi_aqara": "To nie jest bramka Xiaomi Aqara, wykryte urz\u0105dzenie nie pasuje do znanych bramek."
},
"error": {
@@ -10,7 +10,7 @@
"invalid_host": "Adres IP jest nieprawid\u0142owy, po pomoc w rozwi\u0105zaniu problemu wejd\u017a tutaj: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
"invalid_interface": "Nieprawid\u0142owy interfejs sieciowy.",
"invalid_key": "Nieprawid\u0142owy klucz bramki.",
- "invalid_mac": "Nieprawid\u0142owy adres MAC.",
+ "invalid_mac": "Nieprawid\u0142owy adres MAC",
"not_found_error": "Nie mo\u017cna odnale\u017a\u0107 wykrytej bramki, aby uzyska\u0107 niezb\u0119dne informacje, spr\u00f3buj u\u017cy\u0107 adresu IP urz\u0105dzenia, na kt\u00f3rym pracuje Home Assistant jako interfejsu."
},
"flow_title": "Bramka Xiaomi Aqara: {name}",
diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json
index ddce68aa2bf1cf..690d1eb3067c18 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ru.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ru.json
@@ -2,7 +2,7 @@
"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": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "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.",
"not_xiaomi_aqara": "\u042d\u0442\u043e \u043d\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara. \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u043c \u0448\u043b\u044e\u0437\u0430\u043c."
},
"error": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json
new file mode 100644
index 00000000000000..02e2a01961b045
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86",
+ "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d",
+ "not_xiaomi_aqara": "\u8fd9\u4e0d\u662f\u5c0f\u7c73 Aqara \u7f51\u5173\u3002\u53d1\u73b0\u7684\u8bbe\u5907\u4e0e\u5df2\u77e5\u7f51\u5173\u4e0d\u5339\u914d"
+ },
+ "error": {
+ "discovery_error": "\u672a\u53d1\u73b0\u5c0f\u7c73 Aqara \u7f51\u5173\u3002\u8bf7\u5c1d\u8bd5\u4f7f\u7528\u8fd0\u884c Home Assistant \u7684\u8bbe\u5907 IP \u4f5c\u4e3a\u63a5\u53e3",
+ "invalid_host": "IP \u5730\u5740\u65e0\u6548\u3002\u8bf7\u53c2\u9605 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_interface": "\u7f51\u7edc\u63a5\u53e3\u65e0\u6548",
+ "invalid_key": "\u7f51\u5173 key \u65e0\u6548",
+ "invalid_mac": "MAC \u5730\u5740\u65e0\u6548",
+ "not_found_error": "Zeroconf \u53d1\u73b0\u7684\u7f51\u5173\u65e0\u6cd5\u5b9a\u4f4d\uff0c\u56e0\u6b64\u65e0\u6cd5\u83b7\u5f97\u5fc5\u8981\u4fe1\u606f\u3002\u8bf7\u5c1d\u8bd5\u4f7f\u7528\u8fd0\u884c HomeAssistant \u7684\u8bbe\u5907 IP \u4f5c\u4e3a\u63a5\u53e3"
+ },
+ "flow_title": "\u5c0f\u7c73 Aqara \u7f51\u5173\uff1a{name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "\u7f51\u5173 IP"
+ },
+ "description": "\u5982\u679c\u8981\u8fde\u63a5\u5176\u4ed6\u7f51\u5173\uff0c\u8bf7\u518d\u6b21\u8fd0\u884c\u914d\u7f6e\u7a0b\u5e8f",
+ "title": "\u9009\u62e9\u8981\u8fde\u63a5\u7684\u5c0f\u7c73 Aqara \u7f51\u5173"
+ },
+ "settings": {
+ "data": {
+ "key": "\u7f51\u5173 key",
+ "name": "\u7f51\u5173\u540d\u79f0"
+ },
+ "description": "\u6240\u9700\u7684 key \u53ef\u4ee5\u53c2\u8003\u4ee5\u4e0b\u6559\u7a0b\u83b7\u53d6: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz \u3002\u5982\u679c\u672a\u63d0\u4f9b key\uff0c\u5219\u53ea\u80fd\u8bbf\u95ee\u4f20\u611f\u5668",
+ "title": "\u5c0f\u7c73 Aqara \u7f51\u5173\u7684\u53ef\u9009\u8bbe\u7f6e"
+ },
+ "user": {
+ "data": {
+ "host": "IP \u5730\u5740 (\u53ef\u9009)",
+ "interface": "\u8981\u4f7f\u7528\u7684\u7f51\u7edc\u63a5\u53e3",
+ "mac": "MAC \u5730\u5740 (\u53ef\u9009)"
+ },
+ "description": "\u8fde\u63a5\u5230\u60a8\u7684\u5c0f\u7c73 Aqara \u7f51\u5173\uff0c\u5982\u679c IP \u548c MAC \u5730\u5740\u7559\u7a7a\uff0c\u5219\u4f7f\u7528\u81ea\u52a8\u53d1\u73b0",
+ "title": "\u5c0f\u7c73 Aqara \u7f51\u5173"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
index 87ea0c6028a0f7..611108c6513c8d 100644
--- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u7db2\u95dc\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "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\u8a2d\u5099\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408"
},
"error": {
diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py
index 29c80faa892ab1..34ebb550adf589 100644
--- a/homeassistant/components/xiaomi_miio/light.py
+++ b/homeassistant/components/xiaomi_miio/light.py
@@ -278,11 +278,6 @@ def __init__(self, name, light, model, unique_id):
self._state = None
self._state_attrs = {ATTR_MODEL: self._model}
- @property
- def should_poll(self):
- """Poll the light."""
- return True
-
@property
def unique_id(self):
"""Return an unique ID."""
diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py
index 15dc1bea8bd713..5dcf1f8d1e7e0d 100644
--- a/homeassistant/components/xiaomi_miio/sensor.py
+++ b/homeassistant/components/xiaomi_miio/sensor.py
@@ -21,6 +21,7 @@
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
PRESSURE_HPA,
TEMP_CELSIUS,
@@ -173,11 +174,6 @@ def __init__(self, name, device, model, unique_id):
ATTR_MODEL: self._model,
}
- @property
- def should_poll(self):
- """Poll the miio device."""
- return True
-
@property
def unique_id(self):
"""Return an unique ID."""
@@ -307,7 +303,7 @@ def available(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
- return "lux"
+ return LIGHT_LUX
@property
def device_class(self):
diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json
index 171e358c90ef0f..cc032a341e9fee 100644
--- a/homeassistant/components/xiaomi_miio/strings.json
+++ b/homeassistant/components/xiaomi_miio/strings.json
@@ -14,7 +14,7 @@
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this token is different from the key used by the Xiaomi Aqara integration.",
"data": {
"host": "[%key:common::config_flow::data::ip%]",
- "token": "API Token",
+ "token": "[%key:common::config_flow::data::api_token%]",
"name": "Name of the Gateway"
}
}
@@ -25,7 +25,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "Config flow for this Xiaomi Miio device is already in progress."
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
}
}
}
diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py
index 6dfcc539a44bbb..e406e344ebf644 100644
--- a/homeassistant/components/xiaomi_miio/switch.py
+++ b/homeassistant/components/xiaomi_miio/switch.py
@@ -235,11 +235,6 @@ def __init__(self, name, plug, model, unique_id):
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
- @property
- def should_poll(self):
- """Poll the plug."""
- return True
-
@property
def unique_id(self):
"""Return an unique ID."""
diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json
index 9bd8f91c947ee4..920548d858bc68 100644
--- a/homeassistant/components/xiaomi_miio/translations/ca.json
+++ b/homeassistant/components/xiaomi_miio/translations/ca.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key::common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu Xiaomi Miio ja est\u00e0 en curs."
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs"
},
"error": {
"connect_error": "[%key::common::config_flow::error::cannot_connect%]",
@@ -14,7 +14,7 @@
"data": {
"host": "Adre\u00e7a IP",
"name": "Nom de la passarel\u00b7la",
- "token": "Token de l'API"
+ "token": "Token d'API"
},
"description": "Necessitar\u00e0s el token de l'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest token \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.",
"title": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi"
diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json
index 6ec92566adee3f..d52715249b9212 100644
--- a/homeassistant/components/xiaomi_miio/translations/de.json
+++ b/homeassistant/components/xiaomi_miio/translations/de.json
@@ -8,6 +8,7 @@
"connect_error": "Verbindung fehlgeschlagen",
"no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus."
},
+ "flow_title": "Xiaomi Miio: {name}",
"step": {
"gateway": {
"data": {
diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json
index e4b8cb70be6540..9597f0466af7a5 100644
--- a/homeassistant/components/xiaomi_miio/translations/en.json
+++ b/homeassistant/components/xiaomi_miio/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
- "already_in_progress": "Config flow for this Xiaomi Miio device is already in progress."
+ "already_in_progress": "Configuration flow is already in progress"
},
"error": {
"connect_error": "Failed to connect",
diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json
new file mode 100644
index 00000000000000..ef3998a3baf6d0
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "flow_title": "Xiaomi Miio: {name}",
+ "step": {
+ "gateway": {
+ "data": {
+ "host": "IP aadress"
+ },
+ "title": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga"
+ },
+ "user": {
+ "data": {
+ "gateway": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga"
+ },
+ "description": "Vali seade millega soovid \u00fchenduse luua.",
+ "title": "Xiaomi Miio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json
index 3514d759926c17..6981bc498c7f82 100644
--- a/homeassistant/components/xiaomi_miio/translations/it.json
+++ b/homeassistant/components/xiaomi_miio/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "already_in_progress": "Il flusso di configurazione per questo dispositivo Xiaomi Miio \u00e8 gi\u00e0 in corso."
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso"
},
"error": {
"connect_error": "Impossibile connettersi",
diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json
index cf978d4a015da2..408481875d0c3e 100644
--- a/homeassistant/components/xiaomi_miio/translations/no.json
+++ b/homeassistant/components/xiaomi_miio/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "already_in_progress": "Konfigurasjonsflyt for denne Xiaomi Miio-enheten p\u00e5g\u00e5r allerede."
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede"
},
"error": {
"connect_error": "Tilkobling mislyktes.",
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index 90f191a58c4a9f..0c32eaf77ca7cf 100644
--- a/homeassistant/components/xiaomi_miio/translations/pl.json
+++ b/homeassistant/components/xiaomi_miio/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja tego urz\u0105dzenia Xiaomi Miio jest ju\u017c w toku."
},
"error": {
- "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie."
},
"flow_title": "Xiaomi Miio: {name}",
diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json
index 1c934e2784c508..d839705e1e45e9 100644
--- a/homeassistant/components/xiaomi_miio/translations/ru.json
+++ b/homeassistant/components/xiaomi_miio/translations/ru.json
@@ -2,7 +2,7 @@
"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": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f."
+ "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."
},
"error": {
"connect_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
index 086f449e0caa90..9a54104116af46 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json
@@ -8,7 +8,7 @@
"connect_error": "\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5",
"no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002"
},
- "flow_title": "\u5c0f\u7c73 Miio: {name}",
+ "flow_title": "Xiaomi Miio: {name}",
"step": {
"gateway": {
"data": {
@@ -24,7 +24,7 @@
"gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173"
},
"description": "\u8bf7\u9009\u62e9\u8981\u8fde\u63a5\u7684\u8bbe\u5907\u3002",
- "title": "\u5c0f\u7c73 Miio"
+ "title": "Xiaomi Miio"
}
}
}
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
index 7b459c30803212..dc7833d2037767 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "already_in_progress": "\u5c0f\u7c73 Miio \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002"
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d"
},
"error": {
"connect_error": "\u9023\u7dda\u5931\u6557",
diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py
index 3e414016fc5394..53fc458c50d83f 100644
--- a/homeassistant/components/xiaomi_miio/vacuum.py
+++ b/homeassistant/components/xiaomi_miio/vacuum.py
@@ -484,14 +484,21 @@ def update(self):
self.last_clean = self._vacuum.last_clean_details()
self.dnd_state = self._vacuum.dnd_status()
- self._timers = self._vacuum.timer()
-
self._available = True
except OSError as exc:
_LOGGER.error("Got OSError while fetching the state: %s", exc)
except DeviceException as exc:
_LOGGER.warning("Got exception while fetching the state: %s", exc)
+ # Fetch timers separately, see #38285
+ try:
+ self._timers = self._vacuum.timer()
+ except DeviceException as exc:
+ _LOGGER.debug(
+ "Unable to fetch timers, this may happen on some devices: %s", exc
+ )
+ self._timers = []
+
async def async_clean_zone(self, zone, repeats=1):
"""Clean selected area for the number of repeats indicated."""
for _zone in zone:
diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py
index bfec5b932a8fe8..b21f6d3a3f416d 100644
--- a/homeassistant/components/yamaha_musiccast/media_player.py
+++ b/homeassistant/components/yamaha_musiccast/media_player.py
@@ -137,11 +137,6 @@ def state(self):
return self.status
return self.power
- @property
- def should_poll(self):
- """Push an update after each command."""
- return True
-
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py
index cde115cb12ff6b..957844e519d9ae 100644
--- a/homeassistant/components/yandex_transport/sensor.py
+++ b/homeassistant/components/yandex_transport/sensor.py
@@ -60,7 +60,7 @@ def __init__(self, requester: YandexMapsRequester, stop_id, routes, name):
self._name = name
self._attrs = None
- async def async_update(self):
+ async def async_update(self, *, tries=0):
"""Get the latest data from maps.yandex.ru and update the states."""
attrs = {}
closer_time = None
@@ -73,8 +73,12 @@ async def async_update(self):
key_error,
yandex_reply,
)
+ if tries > 0:
+ return
await self.requester.set_new_session()
- data = (await self.requester.get_stop_info(self._stop_id))["data"]
+ await self.async_update(tries=tries + 1)
+ return
+
stop_name = data["name"]
transport_list = data["transports"]
for transport in transport_list:
diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json
new file mode 100644
index 00000000000000..6930fca0a5b45e
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/de.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Ger\u00e4te"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "ip_address": "IP-Addresse"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "use_music_mode": "Musik-Modus aktivieren"
+ }
+ }
+ }
+ },
+ "title": "Yeelight"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/el.json b/homeassistant/components/yeelight/translations/el.json
new file mode 100644
index 00000000000000..6495e7a489fb7f
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/el.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae"
+ }
+ },
+ "user": {
+ "data": {
+ "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP"
+ },
+ "description": "\u0395\u03ac\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)",
+ "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)",
+ "use_music_mode": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2"
+ },
+ "description": "\u0395\u03ac\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03ba\u03b5\u03bd\u03cc, \u03b8\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1."
+ }
+ }
+ },
+ "title": "Yeelight"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json
index 08c2ca92dea132..fdfbdfc5634a6d 100644
--- a/homeassistant/components/yeelight/translations/es.json
+++ b/homeassistant/components/yeelight/translations/es.json
@@ -15,6 +15,7 @@
},
"user": {
"data": {
+ "host": "Host",
"ip_address": "Direcci\u00f3n IP"
},
"description": "Si dejas la direcci\u00f3n IP vac\u00eda, se usar\u00e1 descubrimiento para encontrar dispositivos."
diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json
new file mode 100644
index 00000000000000..3b2d79a34a77e2
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json
new file mode 100644
index 00000000000000..c04006e2c8f9f7
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/ko.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0 \ubc1c\uacac\ub41c \uc7a5\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "\uc7a5\uce58"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "ip_address": "IP \uc8fc\uc18c"
+ },
+ "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc\ub450\uba74 \uc7a5\uce58\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "\ubaa8\ub378(\uc120\ud0dd \uc0ac\ud56d)",
+ "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc2a4\uc704\uce58 \uc0ac\uc6a9",
+ "save_on_change": "\ubcc0\uacbd\uc2dc \uc0c1\ud0dc \uc800\uc7a5",
+ "transition": "\uc804\ud658 \uc2dc\uac04(ms)",
+ "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654"
+ },
+ "description": "\ubaa8\ub378\uc744 \ube44\uc6cc \ub450\uba74 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub429\ub2c8\ub2e4."
+ }
+ }
+ },
+ "title": "\uc774\ub77c\uc774\ud2b8"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/lb.json b/homeassistant/components/yeelight/translations/lb.json
new file mode 100644
index 00000000000000..a57a41c215995f
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/lb.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Apparat"
+ }
+ },
+ "user": {
+ "description": "Falls Host eidel gelass g\u00ebtt, g\u00ebtt eng automatesch Sich gestart"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Modell (Optionell)",
+ "nightlight_switch": "Nuechtliicht Schalter benotzen",
+ "save_on_change": "Status sp\u00e4icheren bei \u00c4nnerung",
+ "use_music_mode": "Musek Modus aktiv\u00e9ieren"
+ },
+ "description": "Falls Modell eidel gelass g\u00ebtt, g\u00ebtt et automatesch erkannt."
+ }
+ }
+ },
+ "title": "Yeelight"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json
new file mode 100644
index 00000000000000..f9f78ffb6f6f05
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/nl.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk"
+ },
+ "error": {
+ "cannot_connect": "Kon niet verbinden"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Apparaat"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "ip_address": "IP adres"
+ },
+ "description": "Als u host leeg laat, wordt detectie gebruikt om apparaten te vinden."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Model (optioneel)",
+ "nightlight_switch": "Gebruik Nachtlichtschakelaar",
+ "save_on_change": "Bewaar status bij wijziging",
+ "transition": "Overgangstijd (ms)",
+ "use_music_mode": "Schakel de muziekmodus in"
+ },
+ "description": "Als u model leeg laat, wordt het automatisch gedetecteerd."
+ }
+ }
+ },
+ "title": "Yeelight"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json
index 4a2636457aa562..325e3cbc7ae1b5 100644
--- a/homeassistant/components/yeelight/translations/pl.json
+++ b/homeassistant/components/yeelight/translations/pl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci."
},
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"pick_device": {
diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json
new file mode 100644
index 00000000000000..9fdd341e941646
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/sv.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Enhet"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py
index 0aa5dea0687c38..4852e874672ac5 100644
--- a/homeassistant/components/zamg/sensor.py
+++ b/homeassistant/components/zamg/sensor.py
@@ -12,6 +12,7 @@
import voluptuous as vol
from homeassistant.const import (
+ AREA_SQUARE_METERS,
ATTR_ATTRIBUTION,
CONF_LATITUDE,
CONF_LONGITUDE,
@@ -20,6 +21,7 @@
DEGREE,
LENGTH_METERS,
PERCENTAGE,
+ PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
__version__,
@@ -41,8 +43,8 @@
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
SENSOR_TYPES = {
- "pressure": ("Pressure", "hPa", "LDstat hPa", float),
- "pressure_sealevel": ("Pressure at Sea Level", "hPa", "LDred hPa", float),
+ "pressure": ("Pressure", PRESSURE_HPA, "LDstat hPa", float),
+ "pressure_sealevel": ("Pressure at Sea Level", PRESSURE_HPA, "LDred hPa", float),
"humidity": ("Humidity", PERCENTAGE, "RF %", int),
"wind_speed": (
"Wind Speed",
@@ -60,7 +62,12 @@
"wind_max_bearing": ("Top Wind Bearing", DEGREE, f"WSR {DEGREE}", int),
"sun_last_hour": ("Sun Last Hour", PERCENTAGE, f"SO {PERCENTAGE}", int),
"temperature": ("Temperature", TEMP_CELSIUS, f"T {TEMP_CELSIUS}", float),
- "precipitation": ("Precipitation", "l/m²", "N l/m²", float),
+ "precipitation": (
+ "Precipitation",
+ f"l/{AREA_SQUARE_METERS}",
+ f"N l/{AREA_SQUARE_METERS}",
+ float,
+ ),
"dewpoint": ("Dew Point", TEMP_CELSIUS, f"TP {TEMP_CELSIUS}", float),
# The following probably not useful for general consumption,
# but we need them to fill in internal attributes
diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py
index fa2c7d50addddb..30776eabbb3625 100644
--- a/homeassistant/components/zengge/light.py
+++ b/homeassistant/components/zengge/light.py
@@ -97,11 +97,6 @@ def supported_features(self):
"""Flag supported features."""
return SUPPORT_ZENGGE_LED
- @property
- def should_poll(self):
- """Feel free to poll."""
- return True
-
@property
def assumed_state(self):
"""We can report the actual state."""
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 51da3638a9e1c6..68300adbcfe97c 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -1,6 +1,6 @@
"""Support for exposing Home Assistant via Zeroconf."""
-import asyncio
import fnmatch
+from functools import partial
import ipaddress
import logging
import socket
@@ -81,26 +81,21 @@
@singleton(DOMAIN)
async def async_get_instance(hass):
"""Zeroconf instance to be shared with other integrations that use it."""
- return await hass.async_add_executor_job(_get_instance, hass)
+ return await _async_get_instance(hass)
-def _get_instance(hass, default_interface=False, ipv6=True):
- """Create an instance."""
+async def _async_get_instance(hass, **zcargs):
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
- zc_args = {}
- if default_interface:
- zc_args["interfaces"] = InterfaceChoice.Default
- if not ipv6:
- zc_args["ip_version"] = IPVersion.V4Only
+ zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
- zeroconf = HaZeroconf(**zc_args)
+ install_multiple_zeroconf_catcher(zeroconf)
- def stop_zeroconf(_):
+ def _stop_zeroconf(_):
"""Stop Zeroconf."""
zeroconf.ha_close()
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
return zeroconf
@@ -135,24 +130,42 @@ def close(self):
ha_close = Zeroconf.close
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up Zeroconf and make Home Assistant discoverable."""
zc_config = config.get(DOMAIN, {})
- zeroconf = hass.data[DOMAIN] = _get_instance(
- hass,
- default_interface=zc_config.get(
- CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE
- ),
- ipv6=zc_config.get(CONF_IPV6, DEFAULT_IPV6),
+ zc_args = {}
+ if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
+ zc_args["interfaces"] = InterfaceChoice.Default
+ if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
+ zc_args["ip_version"] = IPVersion.V4Only
+
+ zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args)
+
+ async def _async_zeroconf_hass_start(_event):
+ """Expose Home Assistant on zeroconf when it starts.
+
+ Wait till started or otherwise HTTP is not up and running.
+ """
+ uuid = await hass.helpers.instance_id.async_get()
+ await hass.async_add_executor_job(
+ _register_hass_zc_service, hass, zeroconf, uuid
+ )
+
+ async def _async_zeroconf_hass_started(_event):
+ """Start the service browser."""
+
+ await _async_start_zeroconf_browser(hass, zeroconf)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start)
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started
)
- install_multiple_zeroconf_catcher(zeroconf)
+ return True
- # Get instance UUID
- uuid = asyncio.run_coroutine_threadsafe(
- hass.helpers.instance_id.async_get(), hass.loop
- ).result()
+def _register_hass_zc_service(hass, zeroconf, uuid):
+ # Get instance UUID
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
params = {
@@ -199,23 +212,25 @@ def setup(hass, config):
properties=params,
)
- def zeroconf_hass_start(_event):
- """Expose Home Assistant on zeroconf when it starts.
+ _LOGGER.info("Starting Zeroconf broadcast")
+ try:
+ zeroconf.register_service(info)
+ except NonUniqueNameException:
+ _LOGGER.error(
+ "Home Assistant instance with identical name present in the local network"
+ )
- Wait till started or otherwise HTTP is not up and running.
- """
- _LOGGER.info("Starting Zeroconf broadcast")
- try:
- zeroconf.register_service(info)
- except NonUniqueNameException:
- _LOGGER.error(
- "Home Assistant instance with identical name present in the local network"
- )
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start)
+async def _async_start_zeroconf_browser(hass, zeroconf):
+ """Start the zeroconf browser."""
+
+ zeroconf_types = await async_get_zeroconf(hass)
+ homekit_models = await async_get_homekit(hass)
+
+ types = list(zeroconf_types)
- zeroconf_types = {}
- homekit_models = {}
+ if HOMEKIT_TYPE not in zeroconf_types:
+ types.append(HOMEKIT_TYPE)
def service_update(zeroconf, service_type, name, state_change):
"""Service state changed."""
@@ -292,25 +307,8 @@ def service_update(zeroconf, service_type, name, state_change):
)
)
- async def zeroconf_hass_started(_event):
- """Start the service browser."""
- nonlocal zeroconf_types
- nonlocal homekit_models
-
- zeroconf_types = await async_get_zeroconf(hass)
- homekit_models = await async_get_homekit(hass)
-
- types = list(zeroconf_types)
-
- if HOMEKIT_TYPE not in zeroconf_types:
- types.append(HOMEKIT_TYPE)
-
- _LOGGER.debug("Starting Zeroconf browser")
- HaServiceBrowser(zeroconf, types, handlers=[service_update])
-
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STARTED, zeroconf_hass_started)
-
- return True
+ _LOGGER.debug("Starting Zeroconf browser")
+ HaServiceBrowser(zeroconf, types, handlers=[service_update])
def handle_homekit(hass, homekit_models, info) -> bool:
diff --git a/homeassistant/components/zerproc/translations/es.json b/homeassistant/components/zerproc/translations/es.json
index 192afd87e65854..a0bb855fc5ddd2 100644
--- a/homeassistant/components/zerproc/translations/es.json
+++ b/homeassistant/components/zerproc/translations/es.json
@@ -6,7 +6,7 @@
},
"step": {
"confirm": {
- "description": "\u00bfQuieres comenzar a configurar?"
+ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?"
}
}
},
diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index bdfeb7815c5353..a5b409c7116994 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -23,6 +23,7 @@
ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_IEEE,
ATTR_LEVEL,
ATTR_MANUFACTURER,
ATTR_MEMBERS,
@@ -54,7 +55,12 @@
WARNING_DEVICE_STROBE_YES,
)
from .core.group import GroupMember
-from .core.helpers import async_is_bindable_target, get_matched_clusters
+from .core.helpers import (
+ async_is_bindable_target,
+ convert_install_code,
+ get_matched_clusters,
+ qr_to_install_code,
+)
_LOGGER = logging.getLogger(__name__)
@@ -67,9 +73,10 @@
ATTR_DURATION = "duration"
ATTR_GROUP = "group"
ATTR_IEEE_ADDRESS = "ieee_address"
-ATTR_IEEE = "ieee"
+ATTR_INSTALL_CODE = "install_code"
ATTR_SOURCE_IEEE = "source_ieee"
ATTR_TARGET_IEEE = "target_ieee"
+ATTR_QR_CODE = "qr_code"
SERVICE_PERMIT = "permit"
SERVICE_REMOVE = "remove"
@@ -83,23 +90,36 @@
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_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),
+}
+
SERVICE_SCHEMAS = {
SERVICE_PERMIT: vol.Schema(
- {
- vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert,
- vol.Optional(ATTR_DURATION, default=60): vol.All(
- vol.Coerce(int), vol.Range(0, 254)
- ),
- }
+ vol.All(
+ cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
+ SERVICE_PERMIT_PARAMS,
+ )
+ ),
+ IEEE_SERVICE: vol.Schema(
+ vol.All(
+ cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
+ {vol.Required(ATTR_IEEE): EUI64.convert},
+ )
),
- IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}),
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
{
vol.Required(ATTR_IEEE): 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,
- vol.Required(ATTR_ATTRIBUTE): cv.positive_int,
+ vol.Required(ATTR_ATTRIBUTE): vol.Any(int, cv.boolean, cv.string),
vol.Required(ATTR_VALUE): cv.string,
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}
@@ -169,13 +189,7 @@
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
- {
- vol.Required("type"): "zha/devices/permit",
- vol.Optional(ATTR_IEEE, default=None): EUI64.convert,
- vol.Optional(ATTR_DURATION, default=60): vol.All(
- vol.Coerce(int), vol.Range(0, 254)
- ),
- }
+ {vol.Required("type"): "zha/devices/permit", **SERVICE_PERMIT_PARAMS}
)
async def websocket_permit_devices(hass, connection, msg):
"""Permit ZHA zigbee devices."""
@@ -199,7 +213,21 @@ def async_cleanup() -> None:
connection.subscriptions[msg["id"]] = async_cleanup
zha_gateway.async_enable_debug_mode()
- await zha_gateway.application_controller.permit(time_s=duration, node=ieee)
+ if ATTR_SOURCE_IEEE in msg:
+ src_ieee = msg[ATTR_SOURCE_IEEE]
+ code = msg[ATTR_INSTALL_CODE]
+ _LOGGER.debug("Allowing join for %s device with install code", src_ieee)
+ await zha_gateway.application_controller.permit_with_key(
+ time_s=duration, node=src_ieee, code=code
+ )
+ elif ATTR_QR_CODE in msg:
+ src_ieee, code = msg[ATTR_QR_CODE]
+ _LOGGER.debug("Allowing join for %s device with install code", src_ieee)
+ await zha_gateway.application_controller.permit_with_key(
+ time_s=duration, node=src_ieee, code=code
+ )
+ else:
+ await zha_gateway.application_controller.permit(time_s=duration, node=ieee)
connection.send_result(msg["id"])
@@ -826,8 +854,25 @@ def async_load_api(hass):
async def permit(service):
"""Allow devices to join this network."""
- duration = service.data.get(ATTR_DURATION)
- ieee = service.data.get(ATTR_IEEE_ADDRESS)
+ duration = service.data[ATTR_DURATION]
+ ieee = service.data.get(ATTR_IEEE)
+ if ATTR_SOURCE_IEEE in service.data:
+ src_ieee = service.data[ATTR_SOURCE_IEEE]
+ code = service.data[ATTR_INSTALL_CODE]
+ _LOGGER.info("Allowing join for %s device with install code", src_ieee)
+ await application_controller.permit_with_key(
+ time_s=duration, node=src_ieee, code=code
+ )
+ return
+
+ if ATTR_QR_CODE in service.data:
+ src_ieee, code = service.data[ATTR_QR_CODE]
+ _LOGGER.info("Allowing join for %s device with install code", src_ieee)
+ await application_controller.permit_with_key(
+ time_s=duration, node=src_ieee, code=code
+ )
+ return
+
if ieee:
_LOGGER.info("Permitting joins for %ss on %s device", duration, ieee)
else:
@@ -840,7 +885,7 @@ async def permit(service):
async def remove(service):
"""Remove a node from the network."""
- ieee = service.data[ATTR_IEEE_ADDRESS]
+ ieee = service.data[ATTR_IEEE]
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
zha_device = zha_gateway.get_device(ieee)
if zha_device is not None and zha_device.is_coordinator:
diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py
index 2548060c0fd9a8..ba95a0e4bc63f3 100644
--- a/homeassistant/components/zha/binary_sensor.py
+++ b/homeassistant/components/zha/binary_sensor.py
@@ -143,6 +143,25 @@ class Opening(BinarySensor):
DEVICE_CLASS = DEVICE_CLASS_OPENING
+@STRICT_MATCH(
+ channel_names=CHANNEL_ON_OFF,
+ manufacturers="IKEA of Sweden",
+ models=lambda model: isinstance(model, str)
+ and model is not None
+ and model.find("motion") != -1,
+)
+@STRICT_MATCH(
+ channel_names=CHANNEL_ON_OFF,
+ manufacturers="Philips",
+ models={"SML001", "SML002"},
+)
+class Motion(BinarySensor):
+ """ZHA BinarySensor."""
+
+ SENSOR_ATTR = "on_off"
+ DEVICE_CLASS = DEVICE_CLASS_MOTION
+
+
@STRICT_MATCH(channel_names=CHANNEL_ZONE)
class IASZone(BinarySensor):
"""ZHA IAS BinarySensor."""
diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py
index ebc2cd5cd0f9b5..0570070785fbdd 100644
--- a/homeassistant/components/zha/core/channels/base.py
+++ b/homeassistant/components/zha/core/channels/base.py
@@ -57,7 +57,13 @@ async def wrapper(*args, **kwds):
return result
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
- channel.debug("command failed: %s exception: %s", command.__name__, str(ex))
+ channel.debug(
+ "command failed: '%s' args: '%s' kwargs '%s' exception: '%s'",
+ command.__name__,
+ args,
+ kwds,
+ str(ex),
+ )
return ex
return wrapper
diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py
index 9138ea097822d0..a4bd2bf2d40104 100644
--- a/homeassistant/components/zha/core/channels/smartenergy.py
+++ b/homeassistant/components/zha/core/channels/smartenergy.py
@@ -3,7 +3,13 @@
import zigpy.zcl.clusters.smartenergy as smartenergy
-from homeassistant.const import LENGTH_FEET, POWER_WATT, TIME_HOURS, TIME_SECONDS
+from homeassistant.const import (
+ POWER_WATT,
+ TIME_HOURS,
+ TIME_SECONDS,
+ VOLUME_CUBIC_FEET,
+ VOLUME_CUBIC_METERS,
+)
from homeassistant.core import callback
from .. import registries, typing as zha_typing
@@ -61,8 +67,8 @@ class Metering(ZigbeeChannel):
unit_of_measure_map = {
0x00: POWER_WATT,
- 0x01: f"m³/{TIME_HOURS}",
- 0x02: f"{LENGTH_FEET}³/{TIME_HOURS}",
+ 0x01: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}",
+ 0x02: f"{VOLUME_CUBIC_FEET}/{TIME_HOURS}",
0x03: f"ccf/{TIME_HOURS}",
0x04: f"US gal/{TIME_HOURS}",
0x05: f"IMP gal/{TIME_HOURS}",
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index 402c1505415f8d..22f8f0f261de53 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -35,6 +35,7 @@
ATTR_DEVICE_IEEE = "device_ieee"
ATTR_DEVICE_TYPE = "device_type"
ATTR_ENDPOINTS = "endpoints"
+ATTR_ENDPOINT_NAMES = "endpoint_names"
ATTR_ENDPOINT_ID = "endpoint_id"
ATTR_IEEE = "ieee"
ATTR_IN_CLUSTERS = "in_clusters"
@@ -46,6 +47,7 @@
ATTR_MEMBERS = "members"
ATTR_MODEL = "model"
ATTR_NAME = "name"
+ATTR_NEIGHBORS = "neighbors"
ATTR_NODE_DESCRIPTOR = "node_descriptor"
ATTR_NWK = "nwk"
ATTR_OUT_CLUSTERS = "out_clusters"
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index b8229793a4823a..68fb7393cd51b5 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -9,7 +9,7 @@
from zigpy import types
import zigpy.exceptions
-from zigpy.profiles import zha, zll
+from zigpy.profiles import PROFILES
import zigpy.quirks
from zigpy.zcl.clusters.general import Groups
import zigpy.zdo.types as zdo_types
@@ -33,6 +33,7 @@
ATTR_DEVICE_IEEE,
ATTR_DEVICE_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_ENDPOINT_NAMES,
ATTR_ENDPOINTS,
ATTR_IEEE,
ATTR_LAST_SEEN,
@@ -41,6 +42,7 @@
ATTR_MANUFACTURER_CODE,
ATTR_MODEL,
ATTR_NAME,
+ ATTR_NEIGHBORS,
ATTR_NODE_DESCRIPTOR,
ATTR_NWK,
ATTR_POWER_SOURCE,
@@ -436,6 +438,39 @@ def zha_device_info(self):
}
for entity_ref in self.gateway.device_registry[self.ieee]
]
+
+ # Return the neighbor information
+ device_info[ATTR_NEIGHBORS] = [
+ {
+ "device_type": neighbor.neighbor.device_type.name,
+ "rx_on_when_idle": neighbor.neighbor.rx_on_when_idle.name,
+ "relationship": neighbor.neighbor.relationship.name,
+ "extended_pan_id": str(neighbor.neighbor.extended_pan_id),
+ "ieee": str(neighbor.neighbor.ieee),
+ "nwk": str(neighbor.neighbor.nwk),
+ "permit_joining": neighbor.neighbor.permit_joining.name,
+ "depth": str(neighbor.neighbor.depth),
+ "lqi": str(neighbor.neighbor.lqi),
+ }
+ for neighbor in self._zigpy_device.neighbors
+ ]
+
+ # Return endpoint device type Names
+ names = []
+ 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:
+ # DeviceType provides undefined enums
+ names.append({ATTR_NAME: profile.DeviceType(endpoint.device_type).name})
+ else:
+ names.append(
+ {
+ ATTR_NAME: f"unknown {endpoint.device_type} device_type "
+ "of 0x{endpoint.profile_id:04x} profile id"
+ }
+ )
+ device_info[ATTR_ENDPOINT_NAMES] = names
+
reg_device = self.gateway.ha_device_registry.async_get(self.device_id)
if reg_device is not None:
device_info["user_given_name"] = reg_device.name_by_user
@@ -474,7 +509,7 @@ def async_get_std_clusters(self):
CLUSTER_TYPE_OUT: endpoint.out_clusters,
}
for (ep_id, endpoint) in self._zigpy_device.endpoints.items()
- if ep_id != 0 and endpoint.profile_id in (zha.PROFILE_ID, zll.PROFILE_ID)
+ if ep_id != 0 and endpoint.profile_id in PROFILES
}
@callback
diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py
index 7813c7133ad5f7..0e967a7a123a20 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -6,15 +6,19 @@
"""
import asyncio
+import binascii
import collections
import functools
import itertools
import logging
from random import uniform
-from typing import Any, Callable, Iterator, List, Optional
+import re
+from typing import Any, Callable, Iterator, List, Optional, Tuple
+import voluptuous as vol
import zigpy.exceptions
import zigpy.types
+import zigpy.util
from homeassistant.core import State, callback
@@ -205,3 +209,63 @@ async def wrapper(channel, *args, **kwargs):
return wrapper
return decorator
+
+
+def convert_install_code(value: str) -> bytes:
+ """Convert string to install code bytes and validate length."""
+
+ try:
+ code = binascii.unhexlify(value.replace("-", "").lower())
+ except binascii.Error as exc:
+ raise vol.Invalid(f"invalid hex string: {value}") from exc
+
+ if len(code) != 18: # 16 byte code + 2 crc bytes
+ raise vol.Invalid("invalid length of the install code")
+
+ if zigpy.util.convert_install_code(code) is None:
+ raise vol.Invalid("invalid install code")
+
+ return code
+
+
+QR_CODES = (
+ # Consciot
+ r"^([\da-fA-F]{16})\|([\da-fA-F]{36})$",
+ # Enbrighten
+ r"""
+ ^Z:
+ ([0-9a-fA-F]{16}) # IEEE address
+ \$I:
+ ([0-9a-fA-F]{36}) # install code
+ $
+ """,
+ # Aqara
+ r"""
+ \$A:
+ ([0-9a-fA-F]{16}) # IEEE address
+ \$I:
+ ([0-9a-fA-F]{36}) # install code
+ $
+ """,
+)
+
+
+def qr_to_install_code(qr_code: str) -> Tuple[zigpy.types.EUI64, bytes]:
+ """Try to parse the QR code.
+
+ if successful, return a tuple of a EUI64 address and install code.
+ """
+
+ for code_pattern in QR_CODES:
+ match = re.search(code_pattern, qr_code, re.VERBOSE)
+ if match is None:
+ continue
+
+ ieee_hex = binascii.unhexlify(match[1])
+ ieee = zigpy.types.EUI64(ieee_hex[::-1])
+ install_code = match[2]
+ # install_code sanity check
+ install_code = convert_install_code(install_code)
+ return ieee, install_code
+
+ raise vol.Invalid(f"couldn't convert qr code: {qr_code}")
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index 0cc350ba4b8b9c..81521748da0919 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -180,10 +180,12 @@ def weight(self) -> int:
"""
weight = 0
if self.models:
- weight += 401 - len(self.models)
+ weight += 401 - (1 if callable(self.models) else len(self.models))
if self.manufacturers:
- weight += 301 - len(self.manufacturers)
+ weight += 301 - (
+ 1 if callable(self.manufacturers) else len(self.manufacturers)
+ )
weight += 10 * len(self.channel_names)
weight += 5 * len(self.generic_ids)
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index 4bde073a933431..a26cd6cc8e6007 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -4,15 +4,15 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
- "bellows==0.20.1",
+ "bellows==0.20.3",
"pyserial==3.4",
- "zha-quirks==0.0.44",
+ "zha-quirks==0.0.45",
"zigpy-cc==0.5.2",
- "zigpy-deconz==0.9.2",
- "zigpy==0.23.2",
+ "zigpy-deconz==0.10.0",
+ "zigpy==0.25.0",
"zigpy-xbee==0.13.0",
"zigpy-zigate==0.6.2",
- "zigpy-znp==0.1.1"
+ "zigpy-znp==0.2.1"
],
"codeowners": ["@dmulcahey", "@adminiuga"]
}
diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py
index 6e2878f371b8dc..6f06b3818444c2 100644
--- a/homeassistant/components/zha/sensor.py
+++ b/homeassistant/components/zha/sensor.py
@@ -14,8 +14,10 @@
)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
STATE_UNKNOWN,
TEMP_CELSIUS,
)
@@ -25,6 +27,7 @@
from .core import discovery
from .core.const import (
+ CHANNEL_ANALOG_INPUT,
CHANNEL_ELECTRICAL_MEASUREMENT,
CHANNEL_HUMIDITY,
CHANNEL_ILLUMINANCE,
@@ -151,6 +154,13 @@ def formatter(self, value):
return round(float(value * self._multiplier) / self._divisor)
+@STRICT_MATCH(channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi")
+class AnalogInput(Sensor):
+ """Sensor that displays analog input values."""
+
+ SENSOR_ATTR = "present_value"
+
+
@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION)
class Battery(Sensor):
"""Battery sensor of power configuration cluster."""
@@ -234,7 +244,7 @@ class Illuminance(Sensor):
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_ILLUMINANCE
- _unit = "lx"
+ _unit = LIGHT_LUX
@staticmethod
def formatter(value):
@@ -266,7 +276,7 @@ class Pressure(Sensor):
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_PRESSURE
_decimals = 0
- _unit = "hPa"
+ _unit = PRESSURE_HPA
@STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE)
diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml
index 257d1026f7f3fd..74793d6000f571 100644
--- a/homeassistant/components/zha/services.yaml
+++ b/homeassistant/components/zha/services.yaml
@@ -9,6 +9,15 @@ permit:
ieee_address:
description: IEEE address of the node permitting new joins
example: "00:0d:6f:00:05:7d:2d:34"
+ source_ieee:
+ description: IEEE address of the joining device (must be used with install code)
+ example: "00:0a:bf:00:01:10:23:35"
+ install_code:
+ description: Install code of the joining device (must be used with source_ieee)
+ example: "1234-5678-1234-5678-AABB-CCDD-AABB-CCDD-EEFF"
+ qr_code:
+ description: value of the QR install code (different between vendors)
+ example: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051"
remove:
description: Remove a node from the Zigbee network.
diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json
index 915501b2437d6a..93b5cd7ccf5d6b 100644
--- a/homeassistant/components/zha/strings.json
+++ b/homeassistant/components/zha/strings.json
@@ -21,9 +21,9 @@
}
}
},
- "error": { "cannot_connect": "Unable to connect to ZHA device." },
+ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" },
"abort": {
- "single_instance_allowed": "Only a single configuration of ZHA is allowed."
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"device_automation": {
diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json
index c1092875b01bb9..7fb36f115324c3 100644
--- a/homeassistant/components/zha/translations/ca.json
+++ b/homeassistant/components/zha/translations/ca.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de ZHA."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
- "cannot_connect": "No s'ha pogut connectar amb el dispositiu ZHA."
+ "cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json
index d54eec90b5f2ce..d9c78171d943d1 100644
--- a/homeassistant/components/zha/translations/en.json
+++ b/homeassistant/components/zha/translations/en.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Only a single configuration of ZHA is allowed."
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
- "cannot_connect": "Unable to connect to ZHA device."
+ "cannot_connect": "Failed to connect"
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json
new file mode 100644
index 00000000000000..27a7113e3d2c70
--- /dev/null
+++ b/homeassistant/components/zha/translations/et.json
@@ -0,0 +1,57 @@
+{
+ "config": {
+ "step": {
+ "port_config": {
+ "title": "Seaded"
+ }
+ }
+ },
+ "device_automation": {
+ "action_type": {
+ "warn": "Hoiata"
+ },
+ "trigger_subtype": {
+ "both_buttons": "M\u00f5lemad nupud",
+ "button_1": "Esimene nupp",
+ "button_2": "Teine nupp",
+ "button_3": "Kolmas nupp",
+ "button_4": "Neljas nupp",
+ "button_5": "Viies nupp",
+ "button_6": "Kuues nupp",
+ "close": "Sulge",
+ "dim_down": "H\u00e4marda",
+ "dim_up": "Tee heledamaks",
+ "left": "Vasakule",
+ "open": "Ava",
+ "right": "Paremale",
+ "turn_off": "L\u00fclita v\u00e4lja",
+ "turn_on": "L\u00fclita sisse"
+ },
+ "trigger_type": {
+ "device_dropped": "Seade kukkus",
+ "device_flipped": "Seade \" {subtype} \" p\u00f6\u00f6rati \u00fcmber",
+ "device_knocked": "Seadet \" {subtype} \" koputati",
+ "device_offline": "Seade on v\u00f5rgu\u00fchenduseta",
+ "device_rotated": "Seadet \" {subtype} \" keerati",
+ "device_shaken": "Seadet raputati",
+ "device_slid": "Seade \" {subtype} \" libises",
+ "device_tilted": "Seadet kallutati",
+ "remote_button_alt_double_press": "\"{subtype}\" on topeltkl\u00f5psatud (alternatiivre\u017eiim)",
+ "remote_button_alt_long_press": "\"{subtype}\" nuppu vajutati pikalt (alternatiivre\u017eiim)",
+ "remote_button_alt_long_release": "\"{subtype}\" nupp vabastati peale pikka vajutust (alternatiivre\u017eiim)",
+ "remote_button_alt_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)",
+ "remote_button_alt_quintuple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)",
+ "remote_button_alt_short_press": "\"{subtype}\" nuppu vajutati (alternatiivre\u017eiim)",
+ "remote_button_alt_short_release": "\"{subtype}\" nupp vabastati (alternatiivre\u017eiim)",
+ "remote_button_alt_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)",
+ "remote_button_double_press": "\" {subtype} \" on topeltkl\u00f5psatud",
+ "remote_button_long_press": "\" {subtype} \" on pikalt alla vajutatud",
+ "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust",
+ "remote_button_quadruple_press": "\"{subtype}\" nuppu on neljakordselt kl\u00f5psatud",
+ "remote_button_quintuple_press": "\"{subtype}\" nuppu on viiekordselt kl\u00f5psatud",
+ "remote_button_short_press": "\"{subtype}\" nupp on vajutatud",
+ "remote_button_short_release": "\"{subtype}\" nupp vabastati",
+ "remote_button_triple_press": "Nuppu \"{subtype}\" kl\u00f5psati kolm korda"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json
index 8330583cf74a9a..066a45feab1d14 100644
--- a/homeassistant/components/zha/translations/it.json
+++ b/homeassistant/components/zha/translations/it.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di ZHA."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
- "cannot_connect": "Impossibile connettersi al dispositivo ZHA."
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json
index e97a4e27ccd661..6dac24f8256dbf 100644
--- a/homeassistant/components/zha/translations/no.json
+++ b/homeassistant/components/zha/translations/no.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Kun en konfigurasjon av ZHA er tillatt."
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
- "cannot_connect": "Kan ikke koble til ZHA-enhet."
+ "cannot_connect": "Tilkobling mislyktes."
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json
index b2089e0dad8356..5e98269028274f 100644
--- a/homeassistant/components/zha/translations/pl.json
+++ b/homeassistant/components/zha/translations/pl.json
@@ -65,6 +65,7 @@
"device_dropped": "nast\u0105pi upadek urz\u0105dzenia",
"device_flipped": "nast\u0105pi odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
"device_knocked": "nast\u0105pi pukni\u0119cie w urz\u0105dzenie \"{subtype}\"",
+ "device_offline": "Urz\u0105dzenie offline",
"device_rotated": "nast\u0105pi obr\u00f3cenie urz\u0105dzenia \"{subtype}\"",
"device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem",
"device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"",
diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json
index eea55972c62e83..4070d759a90a97 100644
--- a/homeassistant/components/zha/translations/ru.json
+++ b/homeassistant/components/zha/translations/ru.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json
index 5965911d459927..e62d61ac8eac60 100644
--- a/homeassistant/components/zha/translations/zh-Hant.json
+++ b/homeassistant/components/zha/translations/zh-Hant.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 ZHA\u3002"
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
- "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u8a2d\u5099\u3002"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
"pick_radio": {
diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py
new file mode 100644
index 00000000000000..d00cc560f22d7a
--- /dev/null
+++ b/homeassistant/components/zodiac/__init__.py
@@ -0,0 +1,19 @@
+"""The zodiac component."""
+import voluptuous as vol
+
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.discovery import async_load_platform
+
+from .const import DOMAIN
+
+CONFIG_SCHEMA = vol.Schema(
+ {vol.Optional(DOMAIN): {}},
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the zodiac component."""
+ hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
+
+ return True
diff --git a/homeassistant/components/zodiac/const.py b/homeassistant/components/zodiac/const.py
new file mode 100644
index 00000000000000..c3e7f13d5e372b
--- /dev/null
+++ b/homeassistant/components/zodiac/const.py
@@ -0,0 +1,31 @@
+"""Constants for Zodiac."""
+DOMAIN = "zodiac"
+
+# Signs
+SIGN_ARIES = "aries"
+SIGN_TAURUS = "taurus"
+SIGN_GEMINI = "gemini"
+SIGN_CANCER = "cancer"
+SIGN_LEO = "leo"
+SIGN_VIRGO = "virgo"
+SIGN_LIBRA = "libra"
+SIGN_SCORPIO = "scorpio"
+SIGN_SAGITTARIUS = "sagittarius"
+SIGN_CAPRICORN = "capricorn"
+SIGN_AQUARIUS = "aquarius"
+SIGN_PISCES = "pisces"
+
+# Elements
+ELEMENT_FIRE = "fire"
+ELEMENT_AIR = "air"
+ELEMENT_EARTH = "earth"
+ELEMENT_WATER = "water"
+
+# Modality
+MODALITY_CARDINAL = "cardinal"
+MODALITY_FIXED = "fixed"
+MODALITY_MUTABLE = "mutable"
+
+# Attributes
+ATTR_ELEMENT = "element"
+ATTR_MODALITY = "modality"
diff --git a/homeassistant/components/zodiac/manifest.json b/homeassistant/components/zodiac/manifest.json
new file mode 100644
index 00000000000000..9d38c2cff39139
--- /dev/null
+++ b/homeassistant/components/zodiac/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "zodiac",
+ "name": "Zodiac",
+ "documentation": "https://www.home-assistant.io/integrations/zodiac",
+ "codeowners": ["@JulienTant"],
+ "quality_scale": "silver"
+}
diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py
new file mode 100644
index 00000000000000..06bd52f6bf5a1e
--- /dev/null
+++ b/homeassistant/components/zodiac/sensor.py
@@ -0,0 +1,220 @@
+"""Support for tracking the zodiac sign."""
+import logging
+
+from homeassistant.helpers.entity import Entity
+from homeassistant.util.dt import as_local, utcnow
+
+from .const import (
+ ATTR_ELEMENT,
+ ATTR_MODALITY,
+ DOMAIN,
+ ELEMENT_AIR,
+ ELEMENT_EARTH,
+ ELEMENT_FIRE,
+ ELEMENT_WATER,
+ MODALITY_CARDINAL,
+ MODALITY_FIXED,
+ MODALITY_MUTABLE,
+ SIGN_AQUARIUS,
+ SIGN_ARIES,
+ SIGN_CANCER,
+ SIGN_CAPRICORN,
+ SIGN_GEMINI,
+ SIGN_LEO,
+ SIGN_LIBRA,
+ SIGN_PISCES,
+ SIGN_SAGITTARIUS,
+ SIGN_SCORPIO,
+ SIGN_TAURUS,
+ SIGN_VIRGO,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+ZODIAC_BY_DATE = (
+ (
+ (21, 3),
+ (20, 4),
+ SIGN_ARIES,
+ {
+ ATTR_ELEMENT: ELEMENT_FIRE,
+ ATTR_MODALITY: MODALITY_CARDINAL,
+ },
+ ),
+ (
+ (21, 4),
+ (20, 5),
+ SIGN_TAURUS,
+ {
+ ATTR_ELEMENT: ELEMENT_EARTH,
+ ATTR_MODALITY: MODALITY_FIXED,
+ },
+ ),
+ (
+ (21, 5),
+ (21, 6),
+ SIGN_GEMINI,
+ {
+ ATTR_ELEMENT: ELEMENT_AIR,
+ ATTR_MODALITY: MODALITY_MUTABLE,
+ },
+ ),
+ (
+ (22, 6),
+ (22, 7),
+ SIGN_CANCER,
+ {
+ ATTR_ELEMENT: ELEMENT_WATER,
+ ATTR_MODALITY: MODALITY_CARDINAL,
+ },
+ ),
+ (
+ (23, 7),
+ (22, 8),
+ SIGN_LEO,
+ {
+ ATTR_ELEMENT: ELEMENT_FIRE,
+ ATTR_MODALITY: MODALITY_FIXED,
+ },
+ ),
+ (
+ (23, 8),
+ (21, 9),
+ SIGN_VIRGO,
+ {
+ ATTR_ELEMENT: ELEMENT_EARTH,
+ ATTR_MODALITY: MODALITY_MUTABLE,
+ },
+ ),
+ (
+ (22, 9),
+ (22, 10),
+ SIGN_LIBRA,
+ {
+ ATTR_ELEMENT: ELEMENT_AIR,
+ ATTR_MODALITY: MODALITY_CARDINAL,
+ },
+ ),
+ (
+ (23, 10),
+ (22, 11),
+ SIGN_SCORPIO,
+ {
+ ATTR_ELEMENT: ELEMENT_WATER,
+ ATTR_MODALITY: MODALITY_FIXED,
+ },
+ ),
+ (
+ (23, 11),
+ (21, 12),
+ SIGN_SAGITTARIUS,
+ {
+ ATTR_ELEMENT: ELEMENT_FIRE,
+ ATTR_MODALITY: MODALITY_MUTABLE,
+ },
+ ),
+ (
+ (22, 12),
+ (20, 1),
+ SIGN_CAPRICORN,
+ {
+ ATTR_ELEMENT: ELEMENT_EARTH,
+ ATTR_MODALITY: MODALITY_CARDINAL,
+ },
+ ),
+ (
+ (21, 1),
+ (19, 2),
+ SIGN_AQUARIUS,
+ {
+ ATTR_ELEMENT: ELEMENT_AIR,
+ ATTR_MODALITY: MODALITY_FIXED,
+ },
+ ),
+ (
+ (20, 2),
+ (20, 3),
+ SIGN_PISCES,
+ {
+ ATTR_ELEMENT: ELEMENT_WATER,
+ ATTR_MODALITY: MODALITY_MUTABLE,
+ },
+ ),
+)
+
+ZODIAC_ICONS = {
+ SIGN_ARIES: "mdi:zodiac-aries",
+ SIGN_TAURUS: "mdi:zodiac-taurus",
+ SIGN_GEMINI: "mdi:zodiac-gemini",
+ SIGN_CANCER: "mdi:zodiac-cancer",
+ SIGN_LEO: "mdi:zodiac-leo",
+ SIGN_VIRGO: "mdi:zodiac-virgo",
+ SIGN_LIBRA: "mdi:zodiac-libra",
+ SIGN_SCORPIO: "mdi:zodiac-scorpio",
+ SIGN_SAGITTARIUS: "mdi:zodiac-sagittarius",
+ SIGN_CAPRICORN: "mdi:zodiac-capricorn",
+ SIGN_AQUARIUS: "mdi:zodiac-aquarius",
+ SIGN_PISCES: "mdi:zodiac-pisces",
+}
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Zodiac sensor platform."""
+ if discovery_info is None:
+ return
+
+ async_add_entities([ZodiacSensor()], True)
+
+
+class ZodiacSensor(Entity):
+ """Representation of a Zodiac sensor."""
+
+ def __init__(self):
+ """Initialize the zodiac sensor."""
+ self._attrs = None
+ self._state = None
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return DOMAIN
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return "Zodiac"
+
+ @property
+ def device_class(self):
+ """Return the device class of the entity."""
+ return "zodiac__sign"
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ZODIAC_ICONS.get(self._state)
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attrs
+
+ async def async_update(self):
+ """Get the time and updates the state."""
+ today = as_local(utcnow()).date()
+
+ month = int(today.month)
+ day = int(today.day)
+
+ for sign in ZODIAC_BY_DATE:
+ if (month == sign[0][1] and day >= sign[0][0]) or (
+ month == sign[1][1] and day <= sign[1][0]
+ ):
+ self._state = sign[2]
+ self._attrs = sign[3]
+ break
diff --git a/homeassistant/components/zodiac/strings.sensor.json b/homeassistant/components/zodiac/strings.sensor.json
new file mode 100644
index 00000000000000..e33465967e322f
--- /dev/null
+++ b/homeassistant/components/zodiac/strings.sensor.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aries": "Aries",
+ "taurus": "Taurus",
+ "gemini": "Gemini",
+ "cancer": "Cancer",
+ "leo": "Leo",
+ "virgo": "Virgo",
+ "libra": "Libra",
+ "scorpio": "Scorpio",
+ "sagittarius": "Sagittarius",
+ "capricorn": "Capricorn",
+ "aquarius": "Aquarius",
+ "pisces": "Pisces"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.ca.json b/homeassistant/components/zodiac/translations/sensor.ca.json
new file mode 100644
index 00000000000000..f4699838cdeb51
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.ca.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Aquari",
+ "aries": "\u00c0ries",
+ "cancer": "C\u00e0ncer",
+ "capricorn": "Capricorn",
+ "gemini": "Bessons",
+ "leo": "Lle\u00f3",
+ "libra": "Balan\u00e7a",
+ "pisces": "Peixos",
+ "sagittarius": "Sagitari",
+ "scorpio": "Escorp\u00ed",
+ "taurus": "Taure",
+ "virgo": "Verge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.de.json b/homeassistant/components/zodiac/translations/sensor.de.json
new file mode 100644
index 00000000000000..d60bd068b8966e
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.de.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Wassermann",
+ "aries": "Widder",
+ "cancer": "Krebs",
+ "capricorn": "Steinbock",
+ "gemini": "Zwillinge",
+ "leo": "L\u00f6we",
+ "libra": "Waage",
+ "pisces": "Fische",
+ "sagittarius": "Sch\u00fctze",
+ "scorpio": "Skorpion",
+ "taurus": "Stier",
+ "virgo": "Jungfrau"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.el.json b/homeassistant/components/zodiac/translations/sensor.el.json
new file mode 100644
index 00000000000000..df3931e6346285
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.el.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\u03a5\u03b4\u03c1\u03bf\u03c7\u03cc\u03bf\u03c2",
+ "aries": "\u039a\u03c1\u03b9\u03cc\u03c2",
+ "cancer": "\u039a\u03b1\u03c1\u03ba\u03af\u03bd\u03bf\u03c2",
+ "capricorn": "\u0391\u03b9\u03b3\u03cc\u03ba\u03b5\u03c1\u03c9\u03c2",
+ "gemini": "\u0394\u03af\u03b4\u03c5\u03bc\u03bf\u03c2",
+ "leo": "\u039b\u03ad\u03c9\u03bd",
+ "libra": "\u0396\u03c5\u03b3\u03cc\u03c2",
+ "pisces": "\u0399\u03c7\u03b8\u03cd\u03c2",
+ "sagittarius": "\u03a4\u03bf\u03be\u03cc\u03c4\u03b7\u03c2",
+ "scorpio": "\u03a3\u03ba\u03bf\u03c1\u03c0\u03b9\u03cc\u03c2",
+ "taurus": "\u03a4\u03b1\u03cd\u03c1\u03bf\u03c2",
+ "virgo": "\u03a0\u03b1\u03c1\u03b8\u03ad\u03bd\u03bf\u03c2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.en.json b/homeassistant/components/zodiac/translations/sensor.en.json
new file mode 100644
index 00000000000000..cd671e146ed712
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.en.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Aquarius",
+ "aries": "Aries",
+ "cancer": "Cancer",
+ "capricorn": "Capricorn",
+ "gemini": "Gemini",
+ "leo": "Leo",
+ "libra": "Libra",
+ "pisces": "Pisces",
+ "sagittarius": "Sagittarius",
+ "scorpio": "Scorpio",
+ "taurus": "Taurus",
+ "virgo": "Virgo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.es.json b/homeassistant/components/zodiac/translations/sensor.es.json
new file mode 100644
index 00000000000000..fbd9d1bd653245
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.es.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Acuario",
+ "aries": "Aries",
+ "cancer": "C\u00e1ncer",
+ "capricorn": "Capricornio",
+ "gemini": "G\u00e9minis",
+ "leo": "Leo",
+ "libra": "Libra",
+ "pisces": "Piscis",
+ "sagittarius": "Sagitario",
+ "scorpio": "Escorpio",
+ "taurus": "Tauro",
+ "virgo": "Virgo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.et.json b/homeassistant/components/zodiac/translations/sensor.et.json
new file mode 100644
index 00000000000000..caf26a0130e3fd
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.et.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Veevalaja",
+ "aries": "J\u00e4\u00e4r",
+ "cancer": "V\u00e4hk",
+ "capricorn": "Kaljukits",
+ "gemini": "Kaksikud",
+ "leo": "L\u00f5vi",
+ "libra": "Kaalud",
+ "pisces": "Kalad",
+ "sagittarius": "Ambur",
+ "scorpio": "Skorpion",
+ "taurus": "S\u00f5nn",
+ "virgo": "Neitsi"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.fr.json b/homeassistant/components/zodiac/translations/sensor.fr.json
new file mode 100644
index 00000000000000..8c492c29f0b49d
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.fr.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Verseau",
+ "aries": "B\u00e9lier",
+ "cancer": "Cancer",
+ "capricorn": "Capricorne",
+ "gemini": "G\u00e9meaux",
+ "leo": "Lion",
+ "libra": "Balance",
+ "pisces": "Poissons",
+ "sagittarius": "Sagittaire",
+ "scorpio": "Scorpion",
+ "taurus": "Taureau",
+ "virgo": "Vierge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.it.json b/homeassistant/components/zodiac/translations/sensor.it.json
new file mode 100644
index 00000000000000..f814476b9cd36a
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.it.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Acquario",
+ "aries": "Ariete",
+ "cancer": "Cancro",
+ "capricorn": "Capricorno",
+ "gemini": "Gemelli",
+ "leo": "Leone",
+ "libra": "Bilancia",
+ "pisces": "Pesci",
+ "sagittarius": "Sagittario",
+ "scorpio": "Scorpione",
+ "taurus": "Toro",
+ "virgo": "Vergine"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.ko.json b/homeassistant/components/zodiac/translations/sensor.ko.json
new file mode 100644
index 00000000000000..0a9fc83cdeacf9
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.ko.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\ubb3c\ubcd1 \uc790\ub9ac",
+ "aries": "\uc591 \uc790\ub9ac",
+ "cancer": "\uac8c \uc790\ub9ac",
+ "capricorn": "\uc5fc\uc18c \uc790\ub9ac",
+ "gemini": "\uc30d\ub465\uc774 \uc790\ub9ac",
+ "leo": "\uc0ac\uc790 \uc790\ub9ac",
+ "libra": "\ucc9c\uce6d \uc790\ub9ac",
+ "pisces": "\ubb3c\uace0\uae30 \uc790\ub9ac",
+ "sagittarius": "\uad81\uc218 \uc790\ub9ac",
+ "scorpio": "\uc804\uac08 \uc790\ub9ac",
+ "taurus": "\ud669\uc18c \uc790\ub9ac",
+ "virgo": "\ucc98\ub140 \uc790\ub9ac"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.lb.json b/homeassistant/components/zodiac/translations/sensor.lb.json
new file mode 100644
index 00000000000000..65ae5095c39dbe
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.lb.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Waassermann",
+ "aries": "Widder",
+ "cancer": "Kriibs",
+ "capricorn": "Steebock",
+ "gemini": "Zwillinge",
+ "leo": "L\u00e9iw",
+ "libra": "Wo",
+ "pisces": "F\u00ebsch",
+ "sagittarius": "Sch\u00ebtz",
+ "scorpio": "Skorpioun",
+ "taurus": "St\u00e9ier",
+ "virgo": "Jongfra"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.nl.json b/homeassistant/components/zodiac/translations/sensor.nl.json
new file mode 100644
index 00000000000000..c07b20de21b819
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.nl.json
@@ -0,0 +1,17 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Waterman",
+ "aries": "Ram",
+ "capricorn": "Steenbok",
+ "gemini": "Tweelingen",
+ "leo": "Leo",
+ "libra": "Weegschaal",
+ "pisces": "Vissen",
+ "sagittarius": "Boogschutter",
+ "scorpio": "Schorpioen",
+ "taurus": "Stier",
+ "virgo": "Maagd"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.no.json b/homeassistant/components/zodiac/translations/sensor.no.json
new file mode 100644
index 00000000000000..dea02eb8ce76d4
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.no.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Vannmannen",
+ "aries": "V\u00e6ren",
+ "cancer": "Kreft",
+ "capricorn": "Steinbukken",
+ "gemini": "Tvillingene",
+ "leo": "L\u00f8ven",
+ "libra": "Vekten",
+ "pisces": "Fiskene",
+ "sagittarius": "Skytten",
+ "scorpio": "Skorpionen",
+ "taurus": "Tyren",
+ "virgo": "Jomfruen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.pl.json b/homeassistant/components/zodiac/translations/sensor.pl.json
new file mode 100644
index 00000000000000..7aecf4724a117c
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.pl.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Wodnik",
+ "aries": "Baran",
+ "cancer": "Rak",
+ "capricorn": "Kozioro\u017cec",
+ "gemini": "Bli\u017ani\u0119ta",
+ "leo": "Lew",
+ "libra": "Waga",
+ "pisces": "Ryby",
+ "sagittarius": "Strzelec",
+ "scorpio": "Skorpion",
+ "taurus": "Byk",
+ "virgo": "Panna"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.ru.json b/homeassistant/components/zodiac/translations/sensor.ru.json
new file mode 100644
index 00000000000000..3a314918428f4b
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.ru.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0435\u0439",
+ "aries": "\u041e\u0432\u0435\u043d",
+ "cancer": "\u0420\u0430\u043a",
+ "capricorn": "\u041a\u043e\u0437\u0435\u0440\u043e\u0433",
+ "gemini": "\u0411\u043b\u0438\u0437\u043d\u0435\u0446\u044b",
+ "leo": "\u041b\u0435\u0432",
+ "libra": "\u0412\u0435\u0441\u044b",
+ "pisces": "\u0420\u044b\u0431\u044b",
+ "sagittarius": "\u0421\u0442\u0440\u0435\u043b\u0435\u0446",
+ "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0438\u043e\u043d",
+ "taurus": "\u0422\u0435\u043b\u0435\u0446",
+ "virgo": "\u0414\u0435\u0432\u0430"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.zh-Hant.json b/homeassistant/components/zodiac/translations/sensor.zh-Hant.json
new file mode 100644
index 00000000000000..938a5b6cbe5d48
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.zh-Hant.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\u6c34\u74f6\u5ea7",
+ "aries": "\u7261\u7f8a\u5ea7",
+ "cancer": "\u5de8\u87f9\u5ea7",
+ "capricorn": "\u6469\u7faf\u5ea7",
+ "gemini": "\u96d9\u5b50\u5ea7",
+ "leo": "\u7345\u5b50\u5ea7",
+ "libra": "\u5929\u79e4\u5ea7",
+ "pisces": "\u96d9\u9b5a\u5ea7",
+ "sagittarius": "\u5c04\u624b\u5ea7",
+ "scorpio": "\u5929\u880d\u5ea7",
+ "taurus": "\u91d1\u725b\u5ea7",
+ "virgo": "\u8655\u5973\u5ea7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/translations/et.json b/homeassistant/components/zone/translations/et.json
index aa921f376e70f4..5452dc408265fc 100644
--- a/homeassistant/components/zone/translations/et.json
+++ b/homeassistant/components/zone/translations/et.json
@@ -1,16 +1,21 @@
{
"config": {
+ "error": {
+ "name_exists": "Nimi on juba kasutusel"
+ },
"step": {
"init": {
"data": {
"icon": "Ikoon",
- "latitude": "Laius",
- "longitude": "Pikkus",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
"name": "Nimi",
+ "passive": "Passiivne",
"radius": "Raadius"
},
- "title": "M\u00e4\u00e4ra tsooni parameetrid"
+ "title": "M\u00e4\u00e4ra ala parameetrid"
}
- }
+ },
+ "title": "Ala"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py
index c631406b0e34f5..92186b7a0b5c78 100644
--- a/homeassistant/components/zoneminder/__init__.py
+++ b/homeassistant/components/zoneminder/__init__.py
@@ -2,97 +2,169 @@
import logging
import voluptuous as vol
-from zoneminder.zm import ZoneMinder
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+import homeassistant.config_entries as config_entries
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ID,
ATTR_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
+ CONF_PLATFORM,
+ CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import config_validation as cv
+
+from . import const
+from .common import (
+ ClientAvailabilityResult,
+ async_test_client_availability,
+ create_client_from_config,
+ del_client_from_data,
+ get_client_from_data,
+ is_client_in_data,
+ set_client_to_data,
+ set_platform_configs,
+)
_LOGGER = logging.getLogger(__name__)
-
-CONF_PATH_ZMS = "path_zms"
-
-DEFAULT_PATH = "/zm/"
-DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
-DEFAULT_SSL = False
-DEFAULT_TIMEOUT = 10
-DEFAULT_VERIFY_SSL = True
-DOMAIN = "zoneminder"
+PLATFORM_DOMAINS = tuple(
+ [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
+)
HOST_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
- vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string,
- vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
+ vol.Optional(CONF_PATH, default=const.DEFAULT_PATH): cv.string,
+ vol.Optional(const.CONF_PATH_ZMS, default=const.DEFAULT_PATH_ZMS): cv.string,
+ vol.Optional(CONF_SSL, default=const.DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=const.DEFAULT_VERIFY_SSL): cv.boolean,
}
)
-CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA
+CONFIG_SCHEMA = vol.All(
+ cv.deprecated(const.DOMAIN, invalidation_version="0.118"),
+ vol.Schema(
+ {const.DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])},
+ extra=vol.ALLOW_EXTRA,
+ ),
)
-SERVICE_SET_RUN_STATE = "set_run_state"
SET_RUN_STATE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
)
-def setup(hass, config):
+async def async_setup(hass: HomeAssistant, base_config: dict):
"""Set up the ZoneMinder component."""
- hass.data[DOMAIN] = {}
+ # Collect the platform specific configs. It's necessary to collect these configs
+ # here instead of the platform's setup_platform function because the invocation order
+ # of setup_platform and async_setup_entry is not consistent.
+ set_platform_configs(
+ hass,
+ SENSOR_DOMAIN,
+ [
+ platform_config
+ for platform_config in base_config.get(SENSOR_DOMAIN, [])
+ if platform_config[CONF_PLATFORM] == const.DOMAIN
+ ],
+ )
+ set_platform_configs(
+ hass,
+ SWITCH_DOMAIN,
+ [
+ platform_config
+ for platform_config in base_config.get(SWITCH_DOMAIN, [])
+ if platform_config[CONF_PLATFORM] == const.DOMAIN
+ ],
+ )
+
+ config = base_config.get(const.DOMAIN)
+
+ if not config:
+ return True
+
+ for config_item in config:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
+ data=config_item,
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up Zoneminder config entry."""
+ zm_client = create_client_from_config(config_entry.data)
- success = True
+ result = await async_test_client_availability(hass, zm_client)
+ if result != ClientAvailabilityResult.AVAILABLE:
+ raise ConfigEntryNotReady
- for conf in config[DOMAIN]:
- protocol = "https" if conf[CONF_SSL] else "http"
+ set_client_to_data(hass, config_entry.unique_id, zm_client)
- host_name = conf[CONF_HOST]
- server_origin = f"{protocol}://{host_name}"
- zm_client = ZoneMinder(
- server_origin,
- conf.get(CONF_USERNAME),
- conf.get(CONF_PASSWORD),
- conf.get(CONF_PATH),
- conf.get(CONF_PATH_ZMS),
- conf.get(CONF_VERIFY_SSL),
+ for platform_domain in PLATFORM_DOMAINS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform_domain)
)
- hass.data[DOMAIN][host_name] = zm_client
-
- success = zm_client.login() and success
-
- def set_active_state(call):
- """Set the ZoneMinder run state to the given state name."""
- zm_id = call.data[ATTR_ID]
- state_name = call.data[ATTR_NAME]
- if zm_id not in hass.data[DOMAIN]:
- _LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
- if not hass.data[DOMAIN][zm_id].set_active_state(state_name):
- _LOGGER.error(
- "Unable to change ZoneMinder state. Host: %s, state: %s",
- zm_id,
- state_name,
+
+ if not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE):
+
+ @callback
+ def set_active_state(call):
+ """Set the ZoneMinder run state to the given state name."""
+ zm_id = call.data[ATTR_ID]
+ state_name = call.data[ATTR_NAME]
+ if not is_client_in_data(hass, zm_id):
+ _LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
+ return
+
+ if not get_client_from_data(hass, zm_id).set_active_state(state_name):
+ _LOGGER.error(
+ "Unable to change ZoneMinder state. Host: %s, state: %s",
+ zm_id,
+ state_name,
+ )
+
+ hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_SET_RUN_STATE,
+ set_active_state,
+ schema=SET_RUN_STATE_SCHEMA,
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Unload Zoneminder config entry."""
+ for platform_domain in PLATFORM_DOMAINS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_unload(
+ config_entry, platform_domain
)
+ )
- hass.services.register(
- DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA
- )
+ # If this is the last config to exist, remove the service too.
+ if len(hass.config_entries.async_entries(const.DOMAIN)) <= 1:
+ hass.services.async_remove(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
- hass.async_create_task(
- async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
- )
+ del_client_from_data(hass, config_entry.unique_id)
- return success
+ return True
diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py
index 739864fdea8054..73f7ce2f4c9b07 100644
--- a/homeassistant/components/zoneminder/binary_sensor.py
+++ b/homeassistant/components/zoneminder/binary_sensor.py
@@ -1,26 +1,43 @@
"""Support for ZoneMinder binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from typing import Callable, List, Optional
-from . import DOMAIN as ZONEMINDER_DOMAIN
+from zoneminder.zm import ZoneMinder
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the ZoneMinder binary sensor platform."""
- sensors = []
- for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items():
- sensors.append(ZMAvailabilitySensor(host_name, zm_client))
- add_entities(sensors)
- return True
+from .common import get_client_from_data
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], Optional[bool]], None],
+) -> None:
+ """Set up the sensor config entry."""
+ zm_client = get_client_from_data(hass, config_entry.unique_id)
+ async_add_entities([ZMAvailabilitySensor(zm_client, config_entry)])
class ZMAvailabilitySensor(BinarySensorEntity):
"""Representation of the availability of ZoneMinder as a binary sensor."""
- def __init__(self, host_name, client):
+ def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
"""Initialize availability sensor."""
self._state = None
- self._name = host_name
+ self._name = config_entry.unique_id
self._client = client
+ self._config_entry = config_entry
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_availability"
@property
def name(self):
@@ -35,7 +52,7 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return "connectivity"
+ return DEVICE_CLASS_CONNECTIVITY
def update(self):
"""Update the state of this sensor (availability of ZoneMinder)."""
diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py
index 6144fe112266ba..c4ef1b14772a25 100644
--- a/homeassistant/components/zoneminder/camera.py
+++ b/homeassistant/components/zoneminder/camera.py
@@ -1,5 +1,8 @@
"""Support for ZoneMinder camera streaming."""
import logging
+from typing import Callable, List, Optional
+
+from zoneminder.monitor import Monitor
from homeassistant.components.mjpeg.camera import (
CONF_MJPEG_URL,
@@ -7,9 +10,12 @@
MjpegCamera,
filter_urllib3_logging,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
-from . import DOMAIN as ZONEMINDER_DOMAIN
+from .common import get_client_from_data
_LOGGER = logging.getLogger(__name__)
@@ -17,23 +23,28 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
filter_urllib3_logging()
- cameras = []
- for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
- monitors = zm_client.get_monitors()
- if not monitors:
- _LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s")
- return
- for monitor in monitors:
- _LOGGER.info("Initializing camera %s", monitor.id)
- cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
- add_entities(cameras)
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], Optional[bool]], None],
+) -> None:
+ """Set up the sensor config entry."""
+ zm_client = get_client_from_data(hass, config_entry.unique_id)
+
+ async_add_entities(
+ [
+ ZoneMinderCamera(monitor, zm_client.verify_ssl, config_entry)
+ for monitor in await hass.async_add_job(zm_client.get_monitors)
+ ]
+ )
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
- def __init__(self, monitor, verify_ssl):
+ def __init__(self, monitor: Monitor, verify_ssl: bool, config_entry: ConfigEntry):
"""Initialize as a subclass of MjpegCamera."""
device_info = {
CONF_NAME: monitor.name,
@@ -45,6 +56,12 @@ def __init__(self, monitor, verify_ssl):
self._is_recording = None
self._is_available = None
self._monitor = monitor
+ self._config_entry = config_entry
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_{self._monitor.id}_camera"
@property
def should_poll(self):
diff --git a/homeassistant/components/zoneminder/common.py b/homeassistant/components/zoneminder/common.py
new file mode 100644
index 00000000000000..9b5498b0cdad90
--- /dev/null
+++ b/homeassistant/components/zoneminder/common.py
@@ -0,0 +1,110 @@
+"""Common code for the ZoneMinder component."""
+from enum import Enum
+from typing import List
+
+import requests
+from zoneminder.zm import ZoneMinder
+
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import HomeAssistant
+
+from . import const
+
+
+def prime_domain_data(hass: HomeAssistant) -> None:
+ """Prime the data structures."""
+ hass.data.setdefault(const.DOMAIN, {})
+
+
+def prime_platform_configs(hass: HomeAssistant, domain: str) -> None:
+ """Prime the data structures."""
+ prime_domain_data(hass)
+ hass.data[const.DOMAIN].setdefault(const.PLATFORM_CONFIGS, {})
+ hass.data[const.DOMAIN][const.PLATFORM_CONFIGS].setdefault(domain, [])
+
+
+def set_platform_configs(hass: HomeAssistant, domain: str, configs: List[dict]) -> None:
+ """Set platform configs."""
+ prime_platform_configs(hass, domain)
+ hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain] = configs
+
+
+def get_platform_configs(hass: HomeAssistant, domain: str) -> List[dict]:
+ """Get platform configs."""
+ prime_platform_configs(hass, domain)
+ return hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain]
+
+
+def prime_config_data(hass: HomeAssistant, unique_id: str) -> None:
+ """Prime the data structures."""
+ prime_domain_data(hass)
+ hass.data[const.DOMAIN].setdefault(const.CONFIG_DATA, {})
+ hass.data[const.DOMAIN][const.CONFIG_DATA].setdefault(unique_id, {})
+
+
+def set_client_to_data(hass: HomeAssistant, unique_id: str, client: ZoneMinder) -> None:
+ """Put a ZoneMinder client in the Home Assistant data."""
+ prime_config_data(hass, unique_id)
+ hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] = client
+
+
+def is_client_in_data(hass: HomeAssistant, unique_id: str) -> bool:
+ """Check if ZoneMinder client is in the Home Assistant data."""
+ prime_config_data(hass, unique_id)
+ return const.API_CLIENT in hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id]
+
+
+def get_client_from_data(hass: HomeAssistant, unique_id: str) -> ZoneMinder:
+ """Get a ZoneMinder client from the Home Assistant data."""
+ prime_config_data(hass, unique_id)
+ return hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
+
+
+def del_client_from_data(hass: HomeAssistant, unique_id: str) -> None:
+ """Delete a ZoneMinder client from the Home Assistant data."""
+ prime_config_data(hass, unique_id)
+ del hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
+
+
+def create_client_from_config(conf: dict) -> ZoneMinder:
+ """Create a new ZoneMinder client from a config."""
+ protocol = "https" if conf[CONF_SSL] else "http"
+
+ host_name = conf[CONF_HOST]
+ server_origin = f"{protocol}://{host_name}"
+
+ return ZoneMinder(
+ server_origin,
+ conf.get(CONF_USERNAME),
+ conf.get(CONF_PASSWORD),
+ conf.get(CONF_PATH),
+ conf.get(const.CONF_PATH_ZMS),
+ conf.get(CONF_VERIFY_SSL),
+ )
+
+
+class ClientAvailabilityResult(Enum):
+ """Client availability test result."""
+
+ AVAILABLE = "available"
+ ERROR_AUTH_FAIL = "invalid_auth"
+ ERROR_CONNECTION_ERROR = "cannot_connect"
+
+
+async def async_test_client_availability(
+ hass: HomeAssistant, client: ZoneMinder
+) -> ClientAvailabilityResult:
+ """Test the availability of a ZoneMinder client."""
+ try:
+ if await hass.async_add_job(client.login):
+ return ClientAvailabilityResult.AVAILABLE
+ return ClientAvailabilityResult.ERROR_AUTH_FAIL
+ except requests.exceptions.ConnectionError:
+ return ClientAvailabilityResult.ERROR_CONNECTION_ERROR
diff --git a/homeassistant/components/zoneminder/config_flow.py b/homeassistant/components/zoneminder/config_flow.py
new file mode 100644
index 00000000000000..8b0b94107f3f7f
--- /dev/null
+++ b/homeassistant/components/zoneminder/config_flow.py
@@ -0,0 +1,99 @@
+"""ZoneMinder config flow."""
+from urllib.parse import urlparse
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SOURCE,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+
+from .common import (
+ ClientAvailabilityResult,
+ async_test_client_availability,
+ create_client_from_config,
+)
+from .const import (
+ CONF_PATH_ZMS,
+ DEFAULT_PATH,
+ DEFAULT_PATH_ZMS,
+ DEFAULT_SSL,
+ DEFAULT_VERIFY_SSL,
+)
+from .const import DOMAIN # pylint: disable=unused-import
+
+
+class ZoneminderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Flow handler for zoneminder integration."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ async def async_step_import(self, config: dict):
+ """Handle a flow initialized by import."""
+ return await self.async_step_finish(
+ {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}}
+ )
+
+ async def async_step_user(self, user_input: dict = None):
+ """Handle user step."""
+ user_input = user_input or {}
+ errors = {}
+
+ if user_input:
+ zm_client = create_client_from_config(user_input)
+ result = await async_test_client_availability(self.hass, zm_client)
+ if result == ClientAvailabilityResult.AVAILABLE:
+ return await self.async_step_finish(user_input)
+
+ errors["base"] = result.value
+
+ return self.async_show_form(
+ step_id=config_entries.SOURCE_USER,
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str,
+ vol.Optional(
+ CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
+ ): str,
+ vol.Optional(
+ CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
+ ): str,
+ vol.Optional(
+ CONF_PATH, default=user_input.get(CONF_PATH, DEFAULT_PATH)
+ ): str,
+ vol.Optional(
+ CONF_PATH_ZMS,
+ default=user_input.get(CONF_PATH_ZMS, DEFAULT_PATH_ZMS),
+ ): str,
+ vol.Optional(
+ CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL)
+ ): bool,
+ vol.Optional(
+ CONF_VERIFY_SSL,
+ default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
+ ): bool,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_finish(self, config: dict):
+ """Finish config flow."""
+ zm_client = create_client_from_config(config)
+ hostname = urlparse(zm_client.get_zms_url()).hostname
+ result = await async_test_client_availability(self.hass, zm_client)
+
+ if result != ClientAvailabilityResult.AVAILABLE:
+ return self.async_abort(reason=str(result.value))
+
+ await self.async_set_unique_id(hostname)
+ self._abort_if_unique_id_configured(config)
+
+ return self.async_create_entry(title=hostname, data=config)
diff --git a/homeassistant/components/zoneminder/const.py b/homeassistant/components/zoneminder/const.py
new file mode 100644
index 00000000000000..ad890a1d4d6c29
--- /dev/null
+++ b/homeassistant/components/zoneminder/const.py
@@ -0,0 +1,14 @@
+"""Constants for zoneminder component."""
+
+CONF_PATH_ZMS = "path_zms"
+
+DEFAULT_PATH = "/zm/"
+DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
+DEFAULT_SSL = False
+DEFAULT_VERIFY_SSL = True
+DOMAIN = "zoneminder"
+SERVICE_SET_RUN_STATE = "set_run_state"
+
+PLATFORM_CONFIGS = "platform_configs"
+CONFIG_DATA = "config_data"
+API_CLIENT = "api_client"
diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json
index b3a87510e5ace0..13bec8c4d9a0f6 100644
--- a/homeassistant/components/zoneminder/manifest.json
+++ b/homeassistant/components/zoneminder/manifest.json
@@ -1,7 +1,8 @@
{
"domain": "zoneminder",
"name": "ZoneMinder",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
"requirements": ["zm-py==0.4.0"],
- "codeowners": ["@rohankapoorcom"]
+ "codeowners": ["@rohankapoorcom", "@vangorra"]
}
diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py
index 75531e79e13b81..8605e842813775 100644
--- a/homeassistant/components/zoneminder/sensor.py
+++ b/homeassistant/components/zoneminder/sensor.py
@@ -1,15 +1,19 @@
"""Support for ZoneMinder sensors."""
import logging
+from typing import Callable, List, Optional
import voluptuous as vol
-from zoneminder.monitor import TimePeriod
+from zoneminder.monitor import Monitor, TimePeriod
+from zoneminder.zm import ZoneMinder
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from . import DOMAIN as ZONEMINDER_DOMAIN
+from .common import get_client_from_data, get_platform_configs
_LOGGER = logging.getLogger(__name__)
@@ -37,35 +41,50 @@
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the ZoneMinder sensor platform."""
- include_archived = config.get(CONF_INCLUDE_ARCHIVED)
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], Optional[bool]], None],
+) -> None:
+ """Set up the sensor config entry."""
+ zm_client = get_client_from_data(hass, config_entry.unique_id)
+ monitors = await hass.async_add_job(zm_client.get_monitors)
+
+ if not monitors:
+ _LOGGER.warning("Did not fetch any monitors from ZoneMinder")
sensors = []
- for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
- monitors = zm_client.get_monitors()
- if not monitors:
- _LOGGER.warning("Could not fetch any monitors from ZoneMinder")
+ for monitor in monitors:
+ sensors.append(ZMSensorMonitors(monitor, config_entry))
- for monitor in monitors:
- sensors.append(ZMSensorMonitors(monitor))
+ for config in get_platform_configs(hass, SENSOR_DOMAIN):
+ include_archived = config.get(CONF_INCLUDE_ARCHIVED)
for sensor in config[CONF_MONITORED_CONDITIONS]:
- sensors.append(ZMSensorEvents(monitor, include_archived, sensor))
+ sensors.append(
+ ZMSensorEvents(monitor, include_archived, sensor, config_entry)
+ )
+
+ sensors.append(ZMSensorRunState(zm_client, config_entry))
- sensors.append(ZMSensorRunState(zm_client))
- add_entities(sensors)
+ async_add_entities(sensors, True)
class ZMSensorMonitors(Entity):
"""Get the status of each ZoneMinder monitor."""
- def __init__(self, monitor):
+ def __init__(self, monitor: Monitor, config_entry: ConfigEntry):
"""Initialize monitor sensor."""
self._monitor = monitor
+ self._config_entry = config_entry
self._state = None
self._is_available = None
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_{self._monitor.id}_status"
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -94,14 +113,26 @@ def update(self):
class ZMSensorEvents(Entity):
"""Get the number of events for each monitor."""
- def __init__(self, monitor, include_archived, sensor_type):
+ def __init__(
+ self,
+ monitor: Monitor,
+ include_archived: bool,
+ sensor_type: str,
+ config_entry: ConfigEntry,
+ ):
"""Initialize event sensor."""
self._monitor = monitor
self._include_archived = include_archived
self.time_period = TimePeriod.get_time_period(sensor_type)
+ self._config_entry = config_entry
self._state = None
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_{self._monitor.id}_{self.time_period.value}_{self._include_archived}_events"
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -125,11 +156,17 @@ def update(self):
class ZMSensorRunState(Entity):
"""Get the ZoneMinder run state."""
- def __init__(self, client):
+ def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
"""Initialize run state sensor."""
self._state = None
self._is_available = None
self._client = client
+ self._config_entry = config_entry
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_runstate"
@property
def name(self):
diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml
index a6fb85b641de10..52e8a3bf0bbcb4 100644
--- a/homeassistant/components/zoneminder/services.yaml
+++ b/homeassistant/components/zoneminder/services.yaml
@@ -1,6 +1,9 @@
set_run_state:
- description: Set the ZoneMinder run state
+ description: "Set the ZoneMinder run state"
fields:
+ id:
+ description: "The host name or IP address of the ZoneMinder instance."
+ example: "10.10.0.2"
name:
- description: The string name of the ZoneMinder run state to set as active.
+ description: "The string name of the ZoneMinder run state to set as active."
example: "Home"
diff --git a/homeassistant/components/zoneminder/strings.json b/homeassistant/components/zoneminder/strings.json
new file mode 100644
index 00000000000000..2973e193b86447
--- /dev/null
+++ b/homeassistant/components/zoneminder/strings.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "title": "Add ZoneMinder Server.",
+ "data": {
+ "host": "Host and Port (ex 10.10.0.4:8010)",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "path": "ZM Path",
+ "path_zms": "ZMS Path",
+ "ssl": "[%key:common::config_flow::data::ssl%]",
+ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
+ }
+ }
+ },
+ "abort": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "create_entry": { "default": "ZoneMinder server added." }
+ }
+}
diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py
index 0428ddbf888f89..d8d1cc78797c15 100644
--- a/homeassistant/components/zoneminder/switch.py
+++ b/homeassistant/components/zoneminder/switch.py
@@ -1,41 +1,61 @@
"""Support for ZoneMinder switches."""
import logging
+from typing import Callable, List, Optional
import voluptuous as vol
-from zoneminder.monitor import MonitorState
+from zoneminder.monitor import Monitor, MonitorState
-from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
+from homeassistant.components.switch import (
+ DOMAIN as SWITCH_DOMAIN,
+ PLATFORM_SCHEMA,
+ SwitchEntity,
+)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
-import homeassistant.helpers.config_validation as cv
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
-from . import DOMAIN as ZONEMINDER_DOMAIN
+from .common import get_client_from_data, get_platform_configs
_LOGGER = logging.getLogger(__name__)
+MONITOR_STATES = {
+ MonitorState[name].value: MonitorState[name]
+ for name in dir(MonitorState)
+ if not name.startswith("_")
+}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
- vol.Required(CONF_COMMAND_ON): cv.string,
- vol.Required(CONF_COMMAND_OFF): cv.string,
+ vol.Required(CONF_COMMAND_ON): vol.All(vol.In(MONITOR_STATES.keys())),
+ vol.Required(CONF_COMMAND_OFF): vol.All(vol.In(MONITOR_STATES.keys())),
}
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the ZoneMinder switch platform."""
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], Optional[bool]], None],
+) -> None:
+ """Set up the sensor config entry."""
+ zm_client = get_client_from_data(hass, config_entry.unique_id)
+ monitors = await hass.async_add_job(zm_client.get_monitors)
- on_state = MonitorState(config.get(CONF_COMMAND_ON))
- off_state = MonitorState(config.get(CONF_COMMAND_OFF))
+ if not monitors:
+ _LOGGER.warning("Could not fetch monitors from ZoneMinder")
+ return
switches = []
- for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
- monitors = zm_client.get_monitors()
- if not monitors:
- _LOGGER.warning("Could not fetch monitors from ZoneMinder")
- return
+ for monitor in monitors:
+ for config in get_platform_configs(hass, SWITCH_DOMAIN):
+ on_state = MONITOR_STATES[config[CONF_COMMAND_ON]]
+ off_state = MONITOR_STATES[config[CONF_COMMAND_OFF]]
+
+ switches.append(
+ ZMSwitchMonitors(monitor, on_state, off_state, config_entry)
+ )
- for monitor in monitors:
- switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
- add_entities(switches)
+ async_add_entities(switches, True)
class ZMSwitchMonitors(SwitchEntity):
@@ -43,13 +63,25 @@ class ZMSwitchMonitors(SwitchEntity):
icon = "mdi:record-rec"
- def __init__(self, monitor, on_state, off_state):
+ def __init__(
+ self,
+ monitor: Monitor,
+ on_state: MonitorState,
+ off_state: MonitorState,
+ config_entry: ConfigEntry,
+ ):
"""Initialize the switch."""
self._monitor = monitor
self._on_state = on_state
self._off_state = off_state
+ self._config_entry = config_entry
self._state = None
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Return a unique ID."""
+ return f"{self._config_entry.unique_id}_{self._monitor.id}_switch_{self._on_state.value}_{self._off_state.value}"
+
@property
def name(self):
"""Return the name of the switch."""
diff --git a/homeassistant/components/zoneminder/translations/ca.json b/homeassistant/components/zoneminder/translations/ca.json
new file mode 100644
index 00000000000000..5be4bbfc8b47c2
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/ca.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "connection_error": "No s'ha pogut connectar al servidor ZoneMinder.",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "create_entry": {
+ "default": "S'ha afegit el servidor ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "connection_error": "No s'ha pogut connectar al servidor ZoneMinder.",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3 i port (ex: 10.10.0.4:8010)",
+ "password": "Contrasenya",
+ "path": "Ruta de ZM",
+ "path_zms": "Ruta de ZMS",
+ "ssl": "Utilitza un certificat SSL",
+ "username": "Nom d'usuari",
+ "verify_ssl": "Verifica el certificat SSL"
+ },
+ "title": "Afegeix un servidor ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json
new file mode 100644
index 00000000000000..1362dcbd62dba3
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/de.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername",
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/el.json b/homeassistant/components/zoneminder/translations/el.json
new file mode 100644
index 00000000000000..81511935386070
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/el.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1.",
+ "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder."
+ },
+ "create_entry": {
+ "default": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1.",
+ "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03b8\u03cd\u03c1\u03b1 (\u03c0\u03c1\u03ce\u03b7\u03bd 10.10.0.4:8010)",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS",
+ "path_zms": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS",
+ "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 SSL \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03bf ZoneMinder",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7",
+ "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL"
+ },
+ "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/en.json b/homeassistant/components/zoneminder/translations/en.json
new file mode 100644
index 00000000000000..eab5f5f1e72449
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/en.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Username or password is incorrect.",
+ "cannot_connect": "Failed to connect",
+ "connection_error": "Failed to connect to a ZoneMinder server.",
+ "invalid_auth": "Invalid authentication"
+ },
+ "create_entry": {
+ "default": "ZoneMinder server added."
+ },
+ "error": {
+ "auth_fail": "Username or password is incorrect.",
+ "cannot_connect": "Failed to connect",
+ "connection_error": "Failed to connect to a ZoneMinder server.",
+ "invalid_auth": "Invalid authentication"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host and Port (ex 10.10.0.4:8010)",
+ "password": "Password",
+ "path": "ZM Path",
+ "path_zms": "ZMS Path",
+ "ssl": "Uses an SSL certificate",
+ "username": "Username",
+ "verify_ssl": "Verify SSL certificate"
+ },
+ "title": "Add ZoneMinder Server."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/es.json b/homeassistant/components/zoneminder/translations/es.json
new file mode 100644
index 00000000000000..7bd3264b2f38a7
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/es.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.",
+ "cannot_connect": "No se pudo conectar",
+ "connection_error": "No se pudo conectar con un servidor ZoneMinder.",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "create_entry": {
+ "default": "Servidor ZoneMinder a\u00f1adido."
+ },
+ "error": {
+ "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.",
+ "cannot_connect": "No se pudo conectar",
+ "connection_error": "No se pudo conectar con un servidor ZoneMinder.",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host y Puerto (ej 10.10.0.4:8010)",
+ "password": "Contrase\u00f1a",
+ "path": "Ruta ZM",
+ "path_zms": "Ruta ZMS",
+ "ssl": "Usar SSL para conexiones a ZoneMinder",
+ "username": "Usuario",
+ "verify_ssl": "Verificar certificado SSL"
+ },
+ "title": "A\u00f1adir Servidor ZoneMinder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/et.json b/homeassistant/components/zoneminder/translations/et.json
new file mode 100644
index 00000000000000..00ec75faee696b
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/et.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Kasutajanimi v\u00f5i salas\u00f5na on vale.",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus.",
+ "invalid_auth": "Tuvastamise viga"
+ },
+ "create_entry": {
+ "default": "ZoneMinderi server on lisatud."
+ },
+ "error": {
+ "auth_fail": "Vale kasutajanimi v\u00f5i salas\u00f5na",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus.",
+ "invalid_auth": "Tuvastamise viga"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host ja port (n\u00e4iteks 10.10.0.4:8010)",
+ "password": "Salas\u00f5na",
+ "path": "ZM aadress",
+ "path_zms": "ZMS-i aadress",
+ "ssl": "Kasutage ZoneMinderiga \u00fchenduse loomiseks SSL-i",
+ "username": "Kasutajanimi",
+ "verify_ssl": "Kontrollige SSL-sertifikaati"
+ },
+ "title": "Lisa ZoneMinderi server."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/fr.json b/homeassistant/components/zoneminder/translations/fr.json
new file mode 100644
index 00000000000000..3f3729ce02f3df
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/fr.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "create_entry": {
+ "default": "Serveur Zoneminder ajout\u00e9."
+ },
+ "error": {
+ "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"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te et port (ex 10.10.0.4:8010)",
+ "password": "Mot de passe",
+ "path": "Chemin ZM",
+ "path_zms": "Chemin ZMS",
+ "ssl": "Utiliser SSL pour les connexions \u00e0 ZoneMinder",
+ "username": "Nom d'utilisateur",
+ "verify_ssl": "V\u00e9rifier le certificat SSL"
+ },
+ "title": "Ajouter le serveur ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/it.json b/homeassistant/components/zoneminder/translations/it.json
new file mode 100644
index 00000000000000..cf2a3a6355369a
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/it.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nome utente o password non corretti.",
+ "cannot_connect": "Impossibile connettersi",
+ "connection_error": "Impossibile connettersi a un server ZoneMinder.",
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "create_entry": {
+ "default": "Server ZoneMinder aggiunto."
+ },
+ "error": {
+ "auth_fail": "Nome utente o password non corretti.",
+ "cannot_connect": "Impossibile connettersi",
+ "connection_error": "Impossibile connettersi a un server ZoneMinder.",
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host e porta (ad es. 10.10.0.4:8010)",
+ "password": "Password",
+ "path": "Percorso ZM",
+ "path_zms": "Percorso ZMS",
+ "ssl": "Utilizza un certificato SSL",
+ "username": "Nome utente",
+ "verify_ssl": "Verificare il certificato SSL"
+ },
+ "title": "Aggiungi Server ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/ko.json b/homeassistant/components/zoneminder/translations/ko.json
new file mode 100644
index 00000000000000..3625d6e402ead4
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/ko.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
+ "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "ZoneMinder \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
+ "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8(\uc608: 10.10.0.4:8010)",
+ "password": "\uc554\ud638",
+ "path": "ZMS \uacbd\ub85c",
+ "path_zms": "ZMS \uacbd\ub85c",
+ "ssl": "ZoneMinder \uc5f0\uacb0\uc5d0 SSL \uc0ac\uc6a9",
+ "username": "\uc0ac\uc6a9\uc790\uba85",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
+ },
+ "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud558\uc138\uc694."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/lb.json b/homeassistant/components/zoneminder/translations/lb.json
new file mode 100644
index 00000000000000..ad0669b1040104
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/lb.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Benotzernumm oder Passwuert inkorrekt",
+ "connection_error": "Feeler beim verbannen mam ZoneMinder Server."
+ },
+ "create_entry": {
+ "default": "Zoneminder Server dob\u00e4igesat."
+ },
+ "error": {
+ "auth_fail": "Benotzernumm oder Passwuert inkorrekt",
+ "connection_error": "Feeler beim verbannen mam ZoneMinder Server."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host a Port (beispill 10.10.0.4:8010)",
+ "password": "Passwuert",
+ "path": "ZM Pad",
+ "path_zms": "ZMS Pad",
+ "ssl": "Benotz SSL fir d'Verbindung mat ZoneMinder",
+ "username": "Benotzernumm",
+ "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen"
+ },
+ "title": "ZoneMinder Server dob\u00e4isetzen."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json
new file mode 100644
index 00000000000000..a9ad121c32e00d
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Gebruikersnaam of wachtwoord is onjuist.",
+ "connection_error": "Kan geen verbinding maken met een ZoneMinder-server."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host en poort (ex 10.10.0.4:8010)",
+ "password": "Wachtwoord",
+ "path": "ZM-pad",
+ "path_zms": "ZMS-pad",
+ "ssl": "Gebruik SSL voor verbindingen met ZoneMinder",
+ "username": "Gebruikersnaam",
+ "verify_ssl": "Verifieer SSLcertificaat"
+ },
+ "title": "Voeg ZoneMinder server toe."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/no.json b/homeassistant/components/zoneminder/translations/no.json
new file mode 100644
index 00000000000000..096f5024ac5427
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/no.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Brukernavn eller passord er feil.",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "connection_error": "Kunne ikke koble til en ZoneMinder-server.",
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "create_entry": {
+ "default": "ZoneMinder-serveren er lagt til."
+ },
+ "error": {
+ "auth_fail": "Brukernavn eller passord er feil.",
+ "cannot_connect": "Tilkobling mislyktes.",
+ "connection_error": "Kunne ikke koble til en ZoneMinder-server.",
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert og port (f.eks. 10.10.0.4:8010)",
+ "password": "Passord",
+ "path": "ZM-bane",
+ "path_zms": "ZMS-bane",
+ "ssl": "Bruker et SSL-sertifikat",
+ "username": "Brukernavn",
+ "verify_ssl": "Verifisere SSL-sertifikat"
+ },
+ "title": "Legg til ZoneMinder Server."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/pl.json b/homeassistant/components/zoneminder/translations/pl.json
new file mode 100644
index 00000000000000..1795b192e7fae5
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/pl.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nazwa u\u017cytkownika lub has\u0142o jest niepoprawne.",
+ "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z serwerem ZoneMinder."
+ },
+ "create_entry": {
+ "default": "Dodano serwer ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "Nazwa u\u017cytkownika lub has\u0142o jest niepoprawne.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z serwerem ZoneMinder."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host i port (np. 10.10.0.4:8010)",
+ "password": "Has\u0142o",
+ "path": "\u015acie\u017cka do ZM",
+ "path_zms": "\u015acie\u017cka do ZMS",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "title": "Dodawanie serwera ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/ru.json b/homeassistant/components/zoneminder/translations/ru.json
new file mode 100644
index 00000000000000..d599e767f64a49
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/ru.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ },
+ "create_entry": {
+ "default": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442 \u0438 \u043f\u043e\u0440\u0442 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 10.10.0.4:8010)",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "path": "\u041f\u0443\u0442\u044c \u043a ZM",
+ "path_zms": "\u041f\u0443\u0442\u044c \u043a ZMS",
+ "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "username": "\u041b\u043e\u0433\u0438\u043d",
+ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
+ },
+ "title": "ZoneMinder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json
new file mode 100644
index 00000000000000..37fd73d32f007a
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/sv.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt."
+ },
+ "error": {
+ "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "verify_ssl": "Verifiera SSL-certifikat"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/zh-Hant.json b/homeassistant/components/zoneminder/translations/zh-Hant.json
new file mode 100644
index 00000000000000..310a15014f6127
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/zh-Hant.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "create_entry": {
+ "default": "ZoneMinder \u4f3a\u670d\u5668\u5df2\u65b0\u589e\u3002"
+ },
+ "error": {
+ "auth_fail": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\uff08\u4f8b\u5982 10.10.0.4:8010\uff09",
+ "password": "\u5bc6\u78bc",
+ "path": "ZM \u8def\u5f91",
+ "path_zms": "ZMS \u8def\u5f91",
+ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31",
+ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
+ },
+ "title": "\u65b0\u589e ZoneMinder \u4f3a\u670d\u5668\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py
index d6c64e914f6cc5..6357d9929b1011 100644
--- a/homeassistant/components/zwave/config_flow.py
+++ b/homeassistant/components/zwave/config_flow.py
@@ -31,7 +31,7 @@ def __init__(self):
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if self._async_current_entries():
- return self.async_abort(reason="one_instance_only")
+ return self.async_abort(reason="single_instance_allowed")
errors = {}
diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json
index cab8e7461cb59e..852b8ca22fab11 100644
--- a/homeassistant/components/zwave/strings.json
+++ b/homeassistant/components/zwave/strings.json
@@ -14,8 +14,8 @@
"option_error": "Z-Wave validation failed. Is the path to the USB stick correct?"
},
"abort": {
- "already_configured": "Z-Wave is already configured",
- "one_instance_only": "Component only supports one Z-Wave instance"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"state": {
@@ -30,4 +30,4 @@
"ready": "Ready"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json
index dedf7f60e4a920..ce4e7f5301b469 100644
--- a/homeassistant/components/zwave/translations/ca.json
+++ b/homeassistant/components/zwave/translations/ca.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave ja est\u00e0 configurat",
- "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave",
+ "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?"
diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json
index bd27966b6a5c13..19732f99e6f36e 100644
--- a/homeassistant/components/zwave/translations/en.json
+++ b/homeassistant/components/zwave/translations/en.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave is already configured",
- "one_instance_only": "Component only supports one Z-Wave instance"
+ "already_configured": "Device is already configured",
+ "one_instance_only": "Component only supports one Z-Wave instance",
+ "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?"
diff --git a/homeassistant/components/zwave/translations/es.json b/homeassistant/components/zwave/translations/es.json
index 02b95f0e0285dd..34dd9dd66530fb 100644
--- a/homeassistant/components/zwave/translations/es.json
+++ b/homeassistant/components/zwave/translations/es.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "Z-Wave ya est\u00e1 configurado",
- "one_instance_only": "El componente solo admite una instancia de Z-Wave"
+ "one_instance_only": "El componente solo admite una instancia de Z-Wave",
+ "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
"option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?"
diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json
index e33b5e32827894..18fc2b7f571a87 100644
--- a/homeassistant/components/zwave/translations/et.json
+++ b/homeassistant/components/zwave/translations/et.json
@@ -1,4 +1,9 @@
{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ }
+ },
"state": {
"_": {
"dead": "Surnud",
@@ -7,8 +12,8 @@
"sleeping": "Ootel"
},
"query_stage": {
- "dead": "Surnud ({query_stage})",
- "initializing": "L\u00e4htestan ( {query_stage} )"
+ "dead": "Surnud",
+ "initializing": "L\u00e4htestan"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json
index 2b1b248e92c475..d8647733fad4af 100644
--- a/homeassistant/components/zwave/translations/it.json
+++ b/homeassistant/components/zwave/translations/it.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave \u00e8 gi\u00e0 configurato",
- "one_instance_only": "Il componente supporta solo un'istanza di Z-Wave"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "one_instance_only": "Il componente supporta solo un'istanza di Z-Wave",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
"option_error": "Convalida Z-Wave fallita. Il percorso della chiavetta USB \u00e8 corretto?"
diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json
index d2614bbb2c7d43..aa1e74264ed802 100644
--- a/homeassistant/components/zwave/translations/no.json
+++ b/homeassistant/components/zwave/translations/no.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave er allerede konfigurert",
- "one_instance_only": "Komponenten st\u00f8tter kun en Z-Wave-forekomst"
+ "already_configured": "Enheten er allerede konfigurert",
+ "one_instance_only": "Komponenten st\u00f8tter kun en Z-Wave-forekomst",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
"option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?"
diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json
index 72255110b6a916..c74405767a247d 100644
--- a/homeassistant/components/zwave/translations/ru.json
+++ b/homeassistant/components/zwave/translations/ru.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
- "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave."
+ "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.",
+ "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave.",
+ "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."
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 36a81f98fa3824..3e9dd27458d3f6 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -33,6 +33,7 @@
CONF_INTERNAL_URL,
CONF_LATITUDE,
CONF_LONGITUDE,
+ CONF_MEDIA_DIRS,
CONF_NAME,
CONF_PACKAGES,
CONF_TEMPERATURE_UNIT,
@@ -221,6 +222,8 @@ def _no_duplicate_auth_mfa_module(
],
_no_duplicate_auth_mfa_module,
),
+ # pylint: disable=no-value-for-parameter
+ vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
}
)
@@ -496,6 +499,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
(CONF_ELEVATION, "elevation"),
(CONF_INTERNAL_URL, "internal_url"),
(CONF_EXTERNAL_URL, "external_url"),
+ (CONF_MEDIA_DIRS, "media_dirs"),
):
if key in config:
setattr(hac, attr, config[key])
@@ -503,8 +507,14 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
if CONF_TIME_ZONE in config:
hac.set_time_zone(config[CONF_TIME_ZONE])
+ if CONF_MEDIA_DIRS not in config:
+ if is_docker_env():
+ hac.media_dirs = {"local": "/media"}
+ else:
+ hac.media_dirs = {"local": hass.config.path("media")}
+
# Init whitelist external dir
- hac.allowlist_external_dirs = {hass.config.path("www"), hass.config.path("media")}
+ hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 347ca294d34e66..139e2066d17e70 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -39,6 +39,9 @@
# been removed and unloaded.
SOURCE_UNIGNORE = "unignore"
+# This is used to signal that re-authentication is required by the user.
+SOURCE_REAUTH = "reauth"
+
HANDLERS = Registry()
STORAGE_KEY = "core.config_entries"
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 81f2243bca3350..d0f17b5de3d914 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,6 +1,6 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 116
+MINOR_VERSION = 117
PATCH_VERSION = "0.dev0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
@@ -116,6 +116,7 @@
CONF_LONGITUDE = "longitude"
CONF_MAC = "mac"
CONF_MAXIMUM = "maximum"
+CONF_MEDIA_DIRS = "media_dirs"
CONF_METHOD = "method"
CONF_MINIMUM = "minimum"
CONF_MODE = "mode"
@@ -380,10 +381,15 @@
# Degree units
DEGREE = "°"
+# Currency units
+CURRENCY_EURO = "€"
+CURRENCY_DOLLAR = "$"
+CURRENCY_CENT = "¢"
+
# Temperature units
TEMP_CELSIUS = f"{DEGREE}C"
TEMP_FAHRENHEIT = f"{DEGREE}F"
-TEMP_KELVIN = f"{DEGREE}K"
+TEMP_KELVIN = "K"
# Time units
TIME_MICROSECONDS = "μs"
@@ -397,6 +403,7 @@
TIME_YEARS = "y"
# Length units
+LENGTH_MILLIMETERS: str = "mm"
LENGTH_CENTIMETERS: str = "cm"
LENGTH_METERS: str = "m"
LENGTH_KILOMETERS: str = "km"
@@ -422,6 +429,7 @@
VOLUME_LITERS: str = "L"
VOLUME_MILLILITERS: str = "mL"
VOLUME_CUBIC_METERS = f"{LENGTH_METERS}³"
+VOLUME_CUBIC_FEET = f"{LENGTH_FEET}³"
VOLUME_GALLONS: str = "gal"
VOLUME_FLUID_OUNCE: str = "fl. oz."
@@ -441,6 +449,9 @@
# Conductivity units
CONDUCTIVITY: str = f"µS/{LENGTH_CENTIMETERS}"
+# Light units
+LIGHT_LUX: str = "lx"
+
# UV Index units
UV_INDEX: str = "UV index"
@@ -461,6 +472,10 @@
SPEED_KILOMETERS_PER_HOUR = f"{LENGTH_KILOMETERS}/{TIME_HOURS}"
SPEED_MILES_PER_HOUR = "mph"
+# Signal_strength units
+SIGNAL_STRENGTH_DECIBELS = "dB"
+SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm"
+
# Data units
DATA_BITS = "bit"
DATA_KILOBITS = "kbit"
@@ -564,6 +579,7 @@
HTTP_OK = 200
HTTP_CREATED = 201
+HTTP_ACCEPTED = 202
HTTP_MOVED_PERMANENTLY = 301
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
@@ -573,6 +589,7 @@
HTTP_UNPROCESSABLE_ENTITY = 422
HTTP_TOO_MANY_REQUESTS = 429
HTTP_INTERNAL_SERVER_ERROR = 500
+HTTP_BAD_GATEWAY = 502
HTTP_SERVICE_UNAVAILABLE = 503
HTTP_BASIC_AUTHENTICATION = "basic"
@@ -607,3 +624,10 @@
# Static list of entities that will never be exposed to
# cloud, alexa, or google_home components
CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"]
+
+# The ID of the Home Assistant Cast App
+CAST_APP_ID_HOMEASSISTANT = "B12CE3CA"
+
+# The tracker error allow when converting
+# loop time to human readable time
+MAX_TIME_TRACKING_ERROR = 0.001
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 8f3809bbd4c4a8..9f598d4641027c 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -21,6 +21,7 @@
Any,
Awaitable,
Callable,
+ Collection,
Coroutine,
Dict,
Iterable,
@@ -538,7 +539,7 @@ def __init__(
event_type: str,
data: Optional[Dict[str, Any]] = None,
origin: EventOrigin = EventOrigin.local,
- time_fired: Optional[int] = None,
+ time_fired: Optional[datetime.datetime] = None,
context: Optional[Context] = None,
) -> None:
"""Initialize a new event."""
@@ -548,6 +549,11 @@ def __init__(
self.time_fired = time_fired or dt_util.utcnow()
self.context: Context = context or Context()
+ def __hash__(self) -> int:
+ """Make hashable."""
+ # The only event type that shares context are the TIME_CHANGED
+ return hash((self.event_type, self.context.id, self.time_fired))
+
def as_dict(self) -> Dict:
"""Create a dict representation of this Event.
@@ -556,8 +562,8 @@ def as_dict(self) -> Dict:
return {
"event_type": self.event_type,
"data": dict(self.data),
- "origin": str(self.origin),
- "time_fired": self.time_fired,
+ "origin": str(self.origin.value),
+ "time_fired": self.time_fired.isoformat(),
"context": self.context.as_dict(),
}
@@ -621,6 +627,7 @@ def async_fire(
event_data: Optional[Dict] = None,
origin: EventOrigin = EventOrigin.local,
context: Optional[Context] = None,
+ time_fired: Optional[datetime.datetime] = None,
) -> None:
"""Fire an event.
@@ -633,7 +640,7 @@ def async_fire(
if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE:
listeners = match_all_listeners + listeners
- event = Event(event_type, event_data, origin, None, context)
+ event = Event(event_type, event_data, origin, time_fired, context)
if event_type != EVENT_TIME_CHANGED:
_LOGGER.debug("Bus:Handling %s", event)
@@ -754,6 +761,7 @@ class State:
last_updated: last time this object was updated.
context: Context in which it was created
domain: Domain of this state.
+ object_id: Object id of this state.
"""
__slots__ = [
@@ -764,6 +772,8 @@ class State:
"last_updated",
"context",
"domain",
+ "object_id",
+ "_as_dict",
]
def __init__(
@@ -797,12 +807,8 @@ def __init__(
self.last_updated = last_updated or dt_util.utcnow()
self.last_changed = last_changed or self.last_updated
self.context = context or Context()
- self.domain = split_entity_id(self.entity_id)[0]
-
- @property
- def object_id(self) -> str:
- """Object id of this state."""
- return split_entity_id(self.entity_id)[1]
+ self.domain, self.object_id = split_entity_id(self.entity_id)
+ self._as_dict: Optional[Dict[str, Collection[Any]]] = None
@property
def name(self) -> str:
@@ -819,14 +825,21 @@ def as_dict(self) -> Dict:
To be used for JSON serialization.
Ensures: state == State.from_dict(state.as_dict())
"""
- return {
- "entity_id": self.entity_id,
- "state": self.state,
- "attributes": dict(self.attributes),
- "last_changed": self.last_changed,
- "last_updated": self.last_updated,
- "context": self.context.as_dict(),
- }
+ if not self._as_dict:
+ 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()
+ self._as_dict = {
+ "entity_id": self.entity_id,
+ "state": self.state,
+ "attributes": dict(self.attributes),
+ "last_changed": last_changed_isoformat,
+ "last_updated": last_updated_isoformat,
+ "context": self.context.as_dict(),
+ }
+ return self._as_dict
@classmethod
def from_dict(cls, json_dict: Dict) -> Any:
@@ -907,7 +920,7 @@ def async_entity_ids(
This method must be run in the event loop.
"""
if domain_filter is None:
- return list(self._states.keys())
+ return list(self._states)
if isinstance(domain_filter, str):
domain_filter = (domain_filter.lower(),)
@@ -918,6 +931,24 @@ def async_entity_ids(
if state.domain in domain_filter
]
+ @callback
+ def async_entity_ids_count(
+ self, domain_filter: Optional[Union[str, Iterable]] = None
+ ) -> int:
+ """Count the entity ids that are being tracked.
+
+ This method must be run in the event loop.
+ """
+ if domain_filter is None:
+ return len(self._states)
+
+ if isinstance(domain_filter, str):
+ domain_filter = (domain_filter.lower(),)
+
+ return len(
+ [None for state in self._states.values() if state.domain in domain_filter]
+ )
+
def all(self, domain_filter: Optional[Union[str, Iterable]] = None) -> List[State]:
"""Create a list of all states."""
return run_callback_threadsafe(
@@ -1390,6 +1421,9 @@ def __init__(self, hass: HomeAssistant) -> None:
# List of allowed external URLs that integrations may use
self.allowlist_external_urls: Set[str] = set()
+ # Dictionary of Media folders that integrations may use
+ self.media_dirs: Dict[str, str] = {}
+
# If Home Assistant is running in safe mode
self.safe_mode: bool = False
@@ -1620,13 +1654,18 @@ def fire_time_event(target: float) -> None:
"""Fire next time event."""
now = dt_util.utcnow()
- hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}, context=timer_context)
+ hass.bus.async_fire(
+ EVENT_TIME_CHANGED, {ATTR_NOW: now}, time_fired=now, context=timer_context
+ )
# If we are more than a second late, a tick was missed
late = monotonic() - target
if late > 1:
hass.bus.async_fire(
- EVENT_TIMER_OUT_OF_SYNC, {ATTR_SECONDS: late}, context=timer_context
+ EVENT_TIMER_OUT_OF_SYNC,
+ {ATTR_SECONDS: late},
+ time_fired=now,
+ context=timer_context,
)
schedule_tick(now)
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 86d778db825666..c7a29e549f7679 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -13,6 +13,7 @@
"agent_dvr",
"airly",
"airvisual",
+ "alarmdecoder",
"almond",
"ambiclimate",
"ambient_station",
@@ -30,6 +31,7 @@
"broadlink",
"brother",
"bsblan",
+ "canary",
"cast",
"cert_expiry",
"control4",
@@ -66,6 +68,7 @@
"geonetnz_volcano",
"gios",
"glances",
+ "goalzero",
"gogogate2",
"gpslogger",
"griddy",
@@ -125,6 +128,7 @@
"nut",
"nws",
"nzbget",
+ "omnilogic",
"onvif",
"opentherm_gw",
"openuv",
@@ -141,16 +145,19 @@
"point",
"poolsense",
"powerwall",
+ "profiler",
"progettihwsw",
"ps4",
"pvpc_hourly_pricing",
"rachio",
"rainmachine",
+ "rfxtrx",
"ring",
"risco",
"roku",
"roomba",
"roon",
+ "rpi_power",
"samsungtv",
"sense",
"sentry",
@@ -179,6 +186,7 @@
"syncthru",
"synology_dsm",
"tado",
+ "tasmota",
"tellduslive",
"tesla",
"tibber",
@@ -212,5 +220,6 @@
"yeelight",
"zerproc",
"zha",
+ "zoneminder",
"zwave"
]
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index 1b09348415c16e..c982b58d8d9933 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -297,7 +297,7 @@ def if_numeric_state(
def state(
hass: HomeAssistant,
entity: Union[None, str, State],
- req_state: Union[str, List[str]],
+ req_state: Any,
for_period: Optional[timedelta] = None,
attribute: Optional[str] = None,
) -> bool:
@@ -314,17 +314,20 @@ def state(
assert isinstance(entity, State)
if attribute is None:
- value = entity.state
+ value: Any = entity.state
else:
- value = str(entity.attributes.get(attribute))
+ value = entity.attributes.get(attribute)
- if isinstance(req_state, str):
+ if not isinstance(req_state, list):
req_state = [req_state]
is_state = False
for req_state_value in req_state:
state_value = req_state_value
- if INPUT_ENTITY_ID.match(req_state_value) is not None:
+ if (
+ isinstance(req_state_value, str)
+ and INPUT_ENTITY_ID.match(req_state_value) is not None
+ ):
state_entity = hass.states.get(req_state_value)
if not state_entity:
continue
@@ -649,13 +652,16 @@ async def async_validate_condition_config(
@callback
-def async_extract_entities(config: ConfigType) -> Set[str]:
+def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]:
"""Extract entities from a condition."""
referenced: Set[str] = set()
to_process = deque([config])
while to_process:
config = to_process.popleft()
+ if isinstance(config, Template):
+ continue
+
condition = config[CONF_CONDITION]
if condition in ("and", "not", "or"):
@@ -674,13 +680,16 @@ def async_extract_entities(config: ConfigType) -> Set[str]:
@callback
-def async_extract_devices(config: ConfigType) -> Set[str]:
+def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]:
"""Extract devices from a condition."""
referenced = set()
to_process = deque([config])
while to_process:
config = to_process.popleft()
+ if isinstance(config, Template):
+ continue
+
condition = config[CONF_CONDITION]
if condition in ("and", "not", "or"):
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
index 2b967286b959a3..f957d884d8d4ab 100644
--- a/homeassistant/helpers/config_entry_flow.py
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -136,7 +136,7 @@ async def async_step_user(
) -> Dict[str, Any]:
"""Handle a user initiated set up flow to create a webhook."""
if not self._allow_multiple and self._async_current_entries():
- return self.async_abort(reason="one_instance_allowed")
+ return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user")
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 282e63e6440ba0..ad514e044aa4d4 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -702,7 +702,7 @@ def deprecated(
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
- # https://github.com/home-assistant/home-assistant/issues/24982
+ # https://github.com/home-assistant/core/issues/24982
module_name = __name__
if replacement_key and invalidation_version:
@@ -929,22 +929,44 @@ def script_action(value: Any) -> dict:
has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
)
-STATE_CONDITION_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Required(CONF_CONDITION): "state",
- vol.Required(CONF_ENTITY_ID): entity_ids,
- vol.Optional(CONF_ATTRIBUTE): str,
- vol.Required(CONF_STATE): vol.Any(str, [str]),
- vol.Optional(CONF_FOR): positive_time_period,
- # To support use_trigger_value in automation
- # Deprecated 2016/04/25
- vol.Optional("from"): str,
- }
- ),
- key_dependency("for", "state"),
+STATE_CONDITION_BASE_SCHEMA = {
+ vol.Required(CONF_CONDITION): "state",
+ vol.Required(CONF_ENTITY_ID): entity_ids,
+ vol.Optional(CONF_ATTRIBUTE): str,
+ vol.Optional(CONF_FOR): positive_time_period,
+ # To support use_trigger_value in automation
+ # Deprecated 2016/04/25
+ vol.Optional("from"): str,
+}
+
+STATE_CONDITION_STATE_SCHEMA = vol.Schema(
+ {
+ **STATE_CONDITION_BASE_SCHEMA,
+ vol.Required(CONF_STATE): vol.Any(str, [str]),
+ }
)
+STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema(
+ {
+ **STATE_CONDITION_BASE_SCHEMA,
+ vol.Required(CONF_STATE): match_all,
+ }
+)
+
+
+def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
+ """Validate a state condition."""
+ if not isinstance(value, dict):
+ raise vol.Invalid("Expected a dictionary")
+
+ if CONF_ATTRIBUTE in value:
+ validated: dict = STATE_CONDITION_ATTRIBUTE_SCHEMA(value)
+ else:
+ validated = STATE_CONDITION_STATE_SCHEMA(value)
+
+ return key_dependency("for", "state")(validated)
+
+
SUN_CONDITION_SCHEMA = vol.All(
vol.Schema(
{
diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py
index 1cf1fa4545c5c7..e686dd2ae4b910 100644
--- a/homeassistant/helpers/data_entry_flow.py
+++ b/homeassistant/helpers/data_entry_flow.py
@@ -8,7 +8,7 @@
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.const import HTTP_NOT_FOUND
+from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND
import homeassistant.helpers.config_validation as cv
@@ -76,7 +76,7 @@ async def post(self, request: web.Request, data: Dict[str, Any]) -> web.Response
except data_entry_flow.UnknownHandler:
return self.json_message("Invalid handler specified", HTTP_NOT_FOUND)
except data_entry_flow.UnknownStep:
- return self.json_message("Handler does not support user", 400)
+ return self.json_message("Handler does not support user", HTTP_BAD_REQUEST)
result = self._prepare_result_json(result)
@@ -107,7 +107,7 @@ async def post(
except data_entry_flow.UnknownFlow:
return self.json_message("Invalid flow specified", HTTP_NOT_FOUND)
except vol.Invalid:
- return self.json_message("User input malformed", 400)
+ return self.json_message("User input malformed", HTTP_BAD_REQUEST)
result = self._prepare_result_json(result)
diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py
index 2a4fafde75bad9..a62a2e6380493a 100644
--- a/homeassistant/helpers/deprecation.py
+++ b/homeassistant/helpers/deprecation.py
@@ -60,7 +60,7 @@ def get_deprecated(
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
- # https://github.com/home-assistant/home-assistant/issues/24982
+ # https://github.com/home-assistant/core/issues/24982
module_name = __name__
logger = logging.getLogger(module_name)
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 5b3366d755430a..11c8535feee9c5 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -362,7 +362,7 @@ def _async_write_ha_state(self) -> None:
else:
extra = (
"Please create a bug report at "
- "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue"
+ "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
)
if self.platform:
extra += (
@@ -453,26 +453,35 @@ async def async_device_update(self, warning: bool = True) -> None:
if self.parallel_updates:
await self.parallel_updates.acquire()
- assert self.hass is not None
- if warning:
- update_warn = self.hass.loop.call_later(
- SLOW_UPDATE_WARNING,
- _LOGGER.warning,
- "Update of %s is taking over %s seconds",
- self.entity_id,
- SLOW_UPDATE_WARNING,
- )
-
try:
# pylint: disable=no-member
if hasattr(self, "async_update"):
- await self.async_update() # type: ignore
+ task = self.hass.async_create_task(self.async_update()) # type: ignore
elif hasattr(self, "update"):
- await self.hass.async_add_executor_job(self.update) # type: ignore
+ task = self.hass.async_add_executor_job(self.update) # type: ignore
+ else:
+ return
+
+ if not warning:
+ await task
+ return
+
+ finished, _ = await asyncio.wait([task], timeout=SLOW_UPDATE_WARNING)
+
+ for done in finished:
+ exc = done.exception()
+ if exc:
+ raise exc
+ return
+
+ _LOGGER.warning(
+ "Update of %s is taking over %s seconds",
+ self.entity_id,
+ SLOW_UPDATE_WARNING,
+ )
+ await task
finally:
self._update_staged = False
- if warning:
- update_warn.cancel()
if self.parallel_updates:
self.parallel_updates.release()
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index da1a3635d728d0..39b09cef193986 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -546,7 +546,7 @@ def async_register_entity_service(self, name, schema, func, required_features=No
async def handle_service(call: ServiceCall) -> None:
"""Handle the service."""
- await service.entity_service_call( # type: ignore
+ await service.entity_service_call(
self.hass,
[
plf
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 8e126c7c14cbc9..b6d59bb500cc19 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -27,6 +27,7 @@
EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED,
MATCH_ALL,
+ MAX_TIME_TRACKING_ERROR,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
)
@@ -40,6 +41,7 @@
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
+from homeassistant.helpers.ratelimit import KeyedRateLimit
from homeassistant.helpers.sun import get_astral_event_next
from homeassistant.helpers.template import RenderInfo, Template, result_as_boolean
from homeassistant.helpers.typing import TemplateVarsType
@@ -47,8 +49,6 @@
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
-MAX_TIME_TRACKING_ERROR = 0.001
-
TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks"
TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener"
@@ -61,19 +61,39 @@
TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks"
TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener"
+_ALL_LISTENER = "all"
+_DOMAINS_LISTENER = "domains"
+_ENTITIES_LISTENER = "entities"
+
_LOGGER = logging.getLogger(__name__)
+@dataclass
+class TrackStates:
+ """Class for keeping track of states being tracked.
+
+ all_states: All states on the system are being tracked
+ entities: Entities to track
+ domains: Domains to track
+ """
+
+ all_states: bool
+ entities: Set
+ domains: Set
+
+
@dataclass
class TrackTemplate:
"""Class for keeping track of a template with variables.
The template is template to calculate.
The variables are variables to pass to the template.
+ The rate_limit is a rate limit on how often the template is re-rendered.
"""
template: Template
variables: TemplateVarsType
+ rate_limit: Optional[timedelta] = None
@dataclass
@@ -448,6 +468,158 @@ def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]:
return [mstr.lower() for mstr in instr]
+class _TrackStateChangeFiltered:
+ """Handle removal / refresh of tracker."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ track_states: TrackStates,
+ action: Callable[[Event], Any],
+ ):
+ """Handle removal / refresh of tracker init."""
+ self.hass = hass
+ self._action = action
+ self._listeners: Dict[str, Callable] = {}
+ self._last_track_states: TrackStates = track_states
+
+ @callback
+ def async_setup(self) -> None:
+ """Create listeners to track states."""
+ track_states = self._last_track_states
+
+ if (
+ not track_states.all_states
+ and not track_states.domains
+ and not track_states.entities
+ ):
+ return
+
+ if track_states.all_states:
+ self._setup_all_listener()
+ return
+
+ self._setup_domains_listener(track_states.domains)
+ self._setup_entities_listener(track_states.domains, track_states.entities)
+
+ @property
+ def listeners(self) -> Dict:
+ """State changes that will cause a re-render."""
+ track_states = self._last_track_states
+ return {
+ _ALL_LISTENER: track_states.all_states,
+ _ENTITIES_LISTENER: track_states.entities,
+ _DOMAINS_LISTENER: track_states.domains,
+ }
+
+ @callback
+ def async_update_listeners(self, new_track_states: TrackStates) -> None:
+ """Update the listeners based on the new TrackStates."""
+ last_track_states = self._last_track_states
+ self._last_track_states = new_track_states
+
+ had_all_listener = last_track_states.all_states
+
+ if new_track_states.all_states:
+ if had_all_listener:
+ return
+ self._cancel_listener(_DOMAINS_LISTENER)
+ self._cancel_listener(_ENTITIES_LISTENER)
+ self._setup_all_listener()
+ return
+
+ if had_all_listener:
+ self._cancel_listener(_ALL_LISTENER)
+
+ domains_changed = new_track_states.domains != last_track_states.domains
+
+ if had_all_listener or domains_changed:
+ domains_changed = True
+ self._cancel_listener(_DOMAINS_LISTENER)
+ self._setup_domains_listener(new_track_states.domains)
+
+ if (
+ had_all_listener
+ or domains_changed
+ or new_track_states.entities != last_track_states.entities
+ ):
+ self._cancel_listener(_ENTITIES_LISTENER)
+ self._setup_entities_listener(
+ new_track_states.domains, new_track_states.entities
+ )
+
+ @callback
+ def async_remove(self) -> None:
+ """Cancel the listeners."""
+ for key in list(self._listeners):
+ self._listeners.pop(key)()
+
+ @callback
+ def _cancel_listener(self, listener_name: str) -> None:
+ if listener_name not in self._listeners:
+ return
+
+ self._listeners.pop(listener_name)()
+
+ @callback
+ def _setup_entities_listener(self, domains: Set, entities: Set) -> None:
+ if domains:
+ entities = entities.copy()
+ entities.update(self.hass.states.async_entity_ids(domains))
+
+ # Entities has changed to none
+ if not entities:
+ return
+
+ self._listeners[_ENTITIES_LISTENER] = async_track_state_change_event(
+ self.hass, entities, self._action
+ )
+
+ @callback
+ def _setup_domains_listener(self, domains: Set) -> None:
+ if not domains:
+ return
+
+ self._listeners[_DOMAINS_LISTENER] = async_track_state_added_domain(
+ self.hass, domains, self._action
+ )
+
+ @callback
+ def _setup_all_listener(self) -> None:
+ self._listeners[_ALL_LISTENER] = self.hass.bus.async_listen(
+ EVENT_STATE_CHANGED, self._action
+ )
+
+
+@callback
+@bind_hass
+def async_track_state_change_filtered(
+ hass: HomeAssistant,
+ track_states: TrackStates,
+ action: Callable[[Event], Any],
+) -> _TrackStateChangeFiltered:
+ """Track state changes with a TrackStates filter that can be updated.
+
+ Parameters
+ ----------
+ hass
+ Home assistant object.
+ track_states
+ A TrackStates data class.
+ action
+ Callable to call with results.
+
+ Returns
+ -------
+ Object used to update the listeners (async_update_listeners) with a new TrackStates or
+ cancel the tracking (async_remove).
+
+ """
+ tracker = _TrackStateChangeFiltered(hass, track_states, action)
+ tracker.async_setup()
+ return tracker
+
+
@callback
@bind_hass
def async_track_template(
@@ -553,18 +725,13 @@ def __init__(
track_template_.template.hass = hass
self._track_templates = track_templates
- self._all_listener: Optional[Callable] = None
- self._domains_listener: Optional[Callable] = None
- self._entities_listener: Optional[Callable] = None
-
self._last_result: Dict[Template, Union[str, TemplateError]] = {}
- self._last_info: Dict[Template, RenderInfo] = {}
+
+ self._rate_limit = KeyedRateLimit(hass)
self._info: Dict[Template, RenderInfo] = {}
- self._last_domains: Set = set()
- self._last_entities: Set = set()
- self._entity_ids_filter: Set = set()
+ self._track_state_changes: Optional[_TrackStateChangeFiltered] = None
- def async_setup(self) -> None:
+ def async_setup(self, raise_on_template_error: bool) -> None:
"""Activation of template tracking."""
for track_template_ in self._track_templates:
template = track_template_.template
@@ -572,213 +739,126 @@ def async_setup(self) -> None:
self._info[template] = template.async_render_to_info(variables)
if self._info[template].exception:
+ if raise_on_template_error:
+ raise self._info[template].exception
_LOGGER.error(
"Error while processing template: %s",
track_template_.template,
exc_info=self._info[template].exception,
)
- self._last_info = self._info.copy()
- self._create_listeners()
+ self._track_state_changes = async_track_state_change_filtered(
+ self.hass, _render_infos_to_track_states(self._info.values()), self._refresh
+ )
+ _LOGGER.debug(
+ "Template group %s listens for %s",
+ self._track_templates,
+ self.listeners,
+ )
@property
def listeners(self) -> Dict:
"""State changes that will cause a re-render."""
- return {
- "all": self._all_listener is not None,
- "entities": self._last_entities,
- "domains": self._last_domains,
- }
-
- @property
- def _needs_all_listener(self) -> bool:
- for track_template_ in self._track_templates:
- template = track_template_.template
-
- # Tracking all states
- if self._info[template].all_states:
- return True
-
- # Previous call had an exception
- # so we do not know which states
- # to track
- if self._info[template].exception:
- return True
-
- return False
-
- @property
- def _all_templates_are_static(self) -> bool:
- for track_template_ in self._track_templates:
- if not self._info[track_template_.template].is_static:
- return False
-
- return True
-
- @callback
- def _create_listeners(self) -> None:
- if self._all_templates_are_static:
- return
-
- if self._needs_all_listener:
- self._setup_all_listener()
- return
-
- self._last_entities, self._last_domains = _entities_domains_from_info(
- self._info.values()
- )
- self._setup_domains_listener(self._last_domains)
- self._setup_entities_listener(self._last_domains, self._last_entities)
+ assert self._track_state_changes
+ return self._track_state_changes.listeners
@callback
- def _cancel_domains_listener(self) -> None:
- if self._domains_listener is None:
- return
- self._domains_listener()
- self._domains_listener = None
-
- @callback
- def _cancel_entities_listener(self) -> None:
- if self._entities_listener is None:
- return
- self._entities_listener()
- self._entities_listener = None
-
- @callback
- def _cancel_all_listener(self) -> None:
- if self._all_listener is None:
- return
- self._all_listener()
- self._all_listener = None
+ def async_remove(self) -> None:
+ """Cancel the listener."""
+ assert self._track_state_changes
+ self._track_state_changes.async_remove()
+ self._rate_limit.async_remove()
@callback
- def _update_listeners(self) -> None:
- if self._needs_all_listener:
- if self._all_listener:
- return
- self._last_domains = set()
- self._last_entities = set()
- self._cancel_domains_listener()
- self._cancel_entities_listener()
- self._setup_all_listener()
- return
+ def async_refresh(self) -> None:
+ """Force recalculate the template."""
+ self._refresh(None)
- had_all_listener = self._all_listener is not None
- if had_all_listener:
- self._cancel_all_listener()
+ def _render_template_if_ready(
+ self,
+ track_template_: TrackTemplate,
+ now: datetime,
+ event: Optional[Event],
+ ) -> Union[bool, TrackTemplateResult]:
+ """Re-render the template if conditions match.
- entities, domains = _entities_domains_from_info(self._info.values())
- domains_changed = domains != self._last_domains
+ Returns False if the template was not be re-rendered
- if had_all_listener or domains_changed:
- domains_changed = True
- self._cancel_domains_listener()
- self._setup_domains_listener(domains)
+ Returns True if the template re-rendered and did not
+ change.
- if had_all_listener or domains_changed or entities != self._last_entities:
- self._cancel_entities_listener()
- self._setup_entities_listener(domains, entities)
+ Returns TrackTemplateResult if the template re-render
+ generates a new result.
+ """
+ template = track_template_.template
- self._last_domains = domains
- self._last_entities = entities
+ if event:
+ info = self._info[template]
- @callback
- def _setup_entities_listener(self, domains: Set, entities: Set) -> None:
- if domains:
- entities = entities.copy()
- entities.update(self.hass.states.async_entity_ids(domains))
+ if not self._rate_limit.async_has_timer(
+ template
+ ) and not _event_triggers_rerender(event, info):
+ return False
- # Entities has changed to none
- if not entities:
- return
+ if self._rate_limit.async_schedule_action(
+ template,
+ _rate_limit_for_event(event, info, track_template_),
+ now,
+ self._refresh,
+ event,
+ ):
+ return False
- self._entities_listener = async_track_state_change_event(
- self.hass, entities, self._refresh
- )
+ _LOGGER.debug(
+ "Template update %s triggered by event: %s",
+ template.template,
+ event,
+ )
- @callback
- def _setup_domains_listener(self, domains: Set) -> None:
- if not domains:
- return
+ self._rate_limit.async_triggered(template, now)
+ self._info[template] = template.async_render_to_info(track_template_.variables)
- self._domains_listener = async_track_state_added_domain(
- self.hass, domains, self._refresh
- )
+ try:
+ result: Union[str, TemplateError] = self._info[template].result()
+ except TemplateError as ex:
+ result = ex
- @callback
- def _setup_all_listener(self) -> None:
- self._all_listener = self.hass.bus.async_listen(
- EVENT_STATE_CHANGED, self._refresh
- )
+ last_result = self._last_result.get(template)
- @callback
- def async_remove(self) -> None:
- """Cancel the listener."""
- self._cancel_all_listener()
- self._cancel_domains_listener()
- self._cancel_entities_listener()
+ # Check to see if the result has changed
+ if result == last_result:
+ return True
- @callback
- def async_refresh(self) -> None:
- """Force recalculate the template."""
- self._refresh(None)
+ if isinstance(result, TemplateError) and isinstance(last_result, TemplateError):
+ return True
- @callback
- def async_update_entity_ids_filter(self, entity_ids: Set) -> None:
- """Update the filtered entity_ids."""
- self._entity_ids_filter = entity_ids
+ return TrackTemplateResult(template, last_result, result)
@callback
def _refresh(self, event: Optional[Event]) -> None:
- entity_id = event and event.data.get(ATTR_ENTITY_ID)
updates = []
info_changed = False
-
- if entity_id and entity_id in self._entity_ids_filter:
- # Skip self-referencing updates
- for track_template_ in self._track_templates:
- _LOGGER.warning(
- "Template loop detected while processing event: %s, skipping template render for Template[%s]",
- event,
- track_template_.template.template,
- )
- return
+ now = dt_util.utcnow()
for track_template_ in self._track_templates:
- template = track_template_.template
- if (
- entity_id
- and len(self._last_info) > 1
- and not self._last_info[template].filter_lifecycle(entity_id)
- ):
+ update = self._render_template_if_ready(track_template_, now, event)
+ if not update:
continue
- self._info[template] = template.async_render_to_info(
- track_template_.variables
- )
info_changed = True
-
- try:
- result: Union[str, TemplateError] = self._info[template].result()
- except TemplateError as ex:
- result = ex
-
- last_result = self._last_result.get(template)
-
- # Check to see if the result has changed
- if result == last_result:
- continue
-
- if isinstance(result, TemplateError) and isinstance(
- last_result, TemplateError
- ):
- continue
-
- updates.append(TrackTemplateResult(template, last_result, result))
+ if isinstance(update, TrackTemplateResult):
+ updates.append(update)
if info_changed:
- self._update_listeners()
- self._last_info = self._info.copy()
+ assert self._track_state_changes
+ self._track_state_changes.async_update_listeners(
+ _render_infos_to_track_states(self._info.values()),
+ )
+ _LOGGER.debug(
+ "Template group %s listens for %s",
+ self._track_templates,
+ self.listeners,
+ )
if not updates:
return
@@ -814,6 +894,7 @@ def async_track_template_result(
hass: HomeAssistant,
track_templates: Iterable[TrackTemplate],
action: TrackTemplateResultListener,
+ raise_on_template_error: bool = False,
) -> _TrackTemplateResultInfo:
"""Add a listener that fires when a the result of a template changes.
@@ -835,9 +916,13 @@ def async_track_template_result(
Home assistant object.
track_templates
An iterable of TrackTemplate.
-
action
Callable to call with results.
+ raise_on_template_error
+ When set to True, if there is an exception
+ processing the template during setup, the system
+ will raise the exception instead of setting up
+ tracking.
Returns
-------
@@ -845,7 +930,7 @@ def async_track_template_result(
"""
tracker = _TrackTemplateResultInfo(hass, track_templates, action)
- tracker.async_setup()
+ tracker.async_setup(raise_on_template_error)
return tracker
@@ -1232,7 +1317,10 @@ def process_state_match(
return lambda state: state in parameter_set
-def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set, Set]:
+@callback
+def _entities_domains_from_render_infos(
+ render_infos: Iterable[RenderInfo],
+) -> Tuple[Set, Set]:
"""Combine from multiple RenderInfo."""
entities = set()
domains = set()
@@ -1242,4 +1330,68 @@ def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set
entities.update(render_info.entities)
if render_info.domains:
domains.update(render_info.domains)
+ if render_info.domains_lifecycle:
+ domains.update(render_info.domains_lifecycle)
return entities, domains
+
+
+@callback
+def _render_infos_needs_all_listener(render_infos: Iterable[RenderInfo]) -> bool:
+ """Determine if an all listener is needed from RenderInfo."""
+ for render_info in render_infos:
+ # Tracking all states
+ if render_info.all_states or render_info.all_states_lifecycle:
+ return True
+
+ # Previous call had an exception
+ # so we do not know which states
+ # to track
+ if render_info.exception:
+ return True
+
+ return False
+
+
+@callback
+def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackStates:
+ """Create a TrackStates dataclass from the latest RenderInfo."""
+ if _render_infos_needs_all_listener(render_infos):
+ return TrackStates(True, set(), set())
+
+ return TrackStates(False, *_entities_domains_from_render_infos(render_infos))
+
+
+@callback
+def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool:
+ """Determine if a template should be re-rendered from an event."""
+ entity_id = event.data.get(ATTR_ENTITY_ID)
+
+ if info.filter(entity_id):
+ return True
+
+ if (
+ event.data.get("new_state") is not None
+ and event.data.get("old_state") is not None
+ ):
+ return False
+
+ return bool(info.filter_lifecycle(entity_id))
+
+
+@callback
+def _rate_limit_for_event(
+ event: Event, info: RenderInfo, track_template_: TrackTemplate
+) -> Optional[timedelta]:
+ """Determine the rate limit for an event."""
+ entity_id = event.data.get(ATTR_ENTITY_ID)
+
+ # Specifically referenced entities are excluded
+ # from the rate limit
+ if entity_id in info.entities:
+ return None
+
+ if track_template_.rate_limit is not None:
+ return track_template_.rate_limit
+
+ rate_limit: Optional[timedelta] = info.rate_limit
+ return rate_limit
diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py
index 8bdfc286c1a4b6..9cff5058a0070e 100644
--- a/homeassistant/helpers/network.py
+++ b/homeassistant/helpers/network.py
@@ -88,10 +88,12 @@ def get_url(
scheme=scheme, host=request_host, port=hass.config.api.port
)
- known_hostname = None
+ known_hostnames = ["localhost"]
if hass.components.hassio.is_hassio():
host_info = hass.components.hassio.get_host_info()
- known_hostname = f"{host_info['hostname']}.local"
+ known_hostnames.extend(
+ [host_info["hostname"], f"{host_info['hostname']}.local"]
+ )
if (
(
@@ -100,7 +102,7 @@ def get_url(
and is_ip_address(request_host)
and is_loopback(ip_address(request_host))
)
- or request_host in ["localhost", known_hostname]
+ or request_host in known_hostnames
)
and (not require_ssl or current_url.scheme == "https")
and (not require_standard_port or current_url.is_default_port())
diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py
new file mode 100644
index 00000000000000..422ebdf2eee7ed
--- /dev/null
+++ b/homeassistant/helpers/ratelimit.py
@@ -0,0 +1,97 @@
+"""Ratelimit helper."""
+import asyncio
+from datetime import datetime, timedelta
+import logging
+from typing import Any, Callable, Dict, Hashable, Optional
+
+from homeassistant.const import MAX_TIME_TRACKING_ERROR
+from homeassistant.core import HomeAssistant, callback
+import homeassistant.util.dt as dt_util
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class KeyedRateLimit:
+ """Class to track rate limits."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ ):
+ """Initialize ratelimit tracker."""
+ self.hass = hass
+ self._last_triggered: Dict[Hashable, datetime] = {}
+ self._rate_limit_timers: Dict[Hashable, asyncio.TimerHandle] = {}
+
+ @callback
+ def async_has_timer(self, key: Hashable) -> bool:
+ """Check if a rate limit timer is running."""
+ return key in self._rate_limit_timers
+
+ @callback
+ def async_triggered(self, key: Hashable, now: Optional[datetime] = None) -> None:
+ """Call when the action we are tracking was triggered."""
+ self.async_cancel_timer(key)
+ self._last_triggered[key] = now or dt_util.utcnow()
+
+ @callback
+ def async_cancel_timer(self, key: Hashable) -> None:
+ """Cancel a rate limit time that will call the action."""
+ if not self.async_has_timer(key):
+ return
+
+ self._rate_limit_timers.pop(key).cancel()
+
+ @callback
+ def async_remove(self) -> None:
+ """Remove all timers."""
+ for timer in self._rate_limit_timers.values():
+ timer.cancel()
+ self._rate_limit_timers.clear()
+
+ @callback
+ def async_schedule_action(
+ self,
+ key: Hashable,
+ rate_limit: Optional[timedelta],
+ now: datetime,
+ action: Callable,
+ *args: Any,
+ ) -> Optional[datetime]:
+ """Check rate limits and schedule an action if we hit the limit.
+
+ If the rate limit is hit:
+ Schedules the action for when the rate limit expires
+ if there are no pending timers. The action must
+ be called in async.
+
+ Returns the time the rate limit will expire
+
+ If the rate limit is not hit:
+
+ Return None
+ """
+ if rate_limit is None or key not in self._last_triggered:
+ return None
+
+ next_call_time = self._last_triggered[key] + rate_limit
+
+ if next_call_time <= now:
+ self.async_cancel_timer(key)
+ return None
+
+ _LOGGER.debug(
+ "Reached rate limit of %s for %s and deferred action until %s",
+ rate_limit,
+ key,
+ next_call_time,
+ )
+
+ if key not in self._rate_limit_timers:
+ self._rate_limit_timers[key] = self.hass.loop.call_later(
+ (next_call_time - now).total_seconds() + MAX_TIME_TRACKING_ERROR,
+ action,
+ *args,
+ )
+
+ return next_call_time
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 717e9c3980c3b4..4d958fe431fd90 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -123,30 +123,71 @@ def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA):
)
+STATIC_VALIDATION_ACTION_TYPES = (
+ cv.SCRIPT_ACTION_CALL_SERVICE,
+ cv.SCRIPT_ACTION_DELAY,
+ cv.SCRIPT_ACTION_WAIT_TEMPLATE,
+ cv.SCRIPT_ACTION_FIRE_EVENT,
+ cv.SCRIPT_ACTION_ACTIVATE_SCENE,
+ cv.SCRIPT_ACTION_VARIABLES,
+)
+
+
+async def async_validate_actions_config(
+ hass: HomeAssistant, actions: List[ConfigType]
+) -> List[ConfigType]:
+ """Validate a list of actions."""
+ return await asyncio.gather(
+ *[async_validate_action_config(hass, action) for action in actions]
+ )
+
+
async def async_validate_action_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
action_type = cv.determine_script_action(config)
- if action_type == cv.SCRIPT_ACTION_DEVICE_AUTOMATION:
+ if action_type in STATIC_VALIDATION_ACTION_TYPES:
+ pass
+
+ elif action_type == cv.SCRIPT_ACTION_DEVICE_AUTOMATION:
platform = await device_automation.async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "action"
)
config = platform.ACTION_SCHEMA(config) # type: ignore
- elif (
- action_type == cv.SCRIPT_ACTION_CHECK_CONDITION
- and config[CONF_CONDITION] == "device"
- ):
- platform = await device_automation.async_get_device_automation_platform(
- hass, config[CONF_DOMAIN], "condition"
- )
- config = platform.CONDITION_SCHEMA(config) # type: ignore
+
+ elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION:
+ if config[CONF_CONDITION] == "device":
+ platform = await device_automation.async_get_device_automation_platform(
+ hass, config[CONF_DOMAIN], "condition"
+ )
+ config = platform.CONDITION_SCHEMA(config) # type: ignore
+
elif action_type == cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER:
config[CONF_WAIT_FOR_TRIGGER] = await async_validate_trigger_config(
hass, config[CONF_WAIT_FOR_TRIGGER]
)
+ elif action_type == cv.SCRIPT_ACTION_REPEAT:
+ config[CONF_SEQUENCE] = await async_validate_actions_config(
+ hass, config[CONF_REPEAT][CONF_SEQUENCE]
+ )
+
+ elif action_type == cv.SCRIPT_ACTION_CHOOSE:
+ if CONF_DEFAULT in config:
+ config[CONF_DEFAULT] = await async_validate_actions_config(
+ hass, config[CONF_DEFAULT]
+ )
+
+ for choose_conf in config[CONF_CHOOSE]:
+ choose_conf[CONF_SEQUENCE] = await async_validate_actions_config(
+ hass, choose_conf[CONF_SEQUENCE]
+ )
+
+ else:
+ raise ValueError(f"No validation for {action_type}")
+
return config
@@ -850,7 +891,7 @@ def referenced_entities(self):
entity_ids = data.get(ATTR_ENTITY_ID)
- if entity_ids is None:
+ if entity_ids is None or isinstance(entity_ids, template.Template):
continue
if isinstance(entity_ids, str):
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index ad5a36467cfe97..20f7aa2d2d76d7 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -13,6 +13,7 @@
Optional,
Set,
Tuple,
+ Union,
)
import voluptuous as vol
@@ -43,10 +44,9 @@
if TYPE_CHECKING:
from homeassistant.helpers.entity import Entity # noqa
+ from homeassistant.helpers.entity_platform import EntityPlatform
-# mypy: allow-untyped-defs, no-check-untyped-defs
-
CONF_SERVICE_ENTITY_ID = "entity_id"
CONF_SERVICE_DATA = "data"
CONF_SERVICE_DATA_TEMPLATE = "data_template"
@@ -340,7 +340,13 @@ def async_set_service_schema(
@bind_hass
-async def entity_service_call(hass, platforms, func, call, required_features=None):
+async def entity_service_call(
+ hass: HomeAssistantType,
+ platforms: Iterable["EntityPlatform"],
+ func: Union[str, Callable[..., Any]],
+ call: ha.ServiceCall,
+ required_features: Optional[Iterable[int]] = None,
+) -> None:
"""Handle an entity service call.
Calls all platforms simultaneously.
@@ -349,7 +355,9 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
- entity_perms = user.permissions.check_entity
+ entity_perms: Optional[
+ Callable[[str, str], bool]
+ ] = user.permissions.check_entity
else:
entity_perms = None
@@ -361,7 +369,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
# If the service function is a string, we'll pass it the service call data
if isinstance(func, str):
- data = {
+ data: Union[Dict, ha.ServiceCall] = {
key: val
for key, val in call.data.items()
if key not in cv.ENTITY_SERVICE_FIELDS
@@ -373,7 +381,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
# Check the permissions
# A list with entities to call the service on.
- entity_candidates = []
+ entity_candidates: List["Entity"] = []
if entity_perms is None:
for platform in platforms:
@@ -435,9 +443,12 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
continue
# Skip entities that don't have the required feature.
- if required_features is not None and not any(
- entity.supported_features & feature_set == feature_set
- for feature_set in required_features
+ if required_features is not None and (
+ entity.supported_features is None
+ or not any(
+ entity.supported_features & feature_set == feature_set
+ for feature_set in required_features
+ )
):
continue
@@ -476,12 +487,18 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non
future.result() # pop exception if have
-async def _handle_entity_call(hass, entity, func, data, context):
+async def _handle_entity_call(
+ hass: HomeAssistantType,
+ entity: "Entity",
+ func: Union[str, Callable[..., Any]],
+ data: Union[Dict, ha.ServiceCall],
+ context: ha.Context,
+) -> None:
"""Handle calling service method."""
entity.async_set_context(context)
if isinstance(func, str):
- result = hass.async_add_job(partial(getattr(entity, func), **data))
+ result = hass.async_add_job(partial(getattr(entity, func), **data)) # type: ignore
else:
result = hass.async_add_job(func, entity, data)
@@ -495,7 +512,7 @@ async def _handle_entity_call(hass, entity, func, data, context):
func,
entity.entity_id,
)
- await result
+ await result # type: ignore
@bind_hass
@@ -530,12 +547,12 @@ async def admin_handler(call: ha.ServiceCall) -> None:
def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable:
"""Ensure permission to access any entity under domain in service call."""
- def decorator(service_handler: Callable) -> Callable:
+ def decorator(service_handler: Callable[[ha.ServiceCall], Any]) -> Callable:
"""Decorate."""
if not asyncio.iscoroutinefunction(service_handler):
raise HomeAssistantError("Can only decorate async functions.")
- async def check_permissions(call):
+ async def check_permissions(call: ha.ServiceCall) -> Any:
"""Check user permission and raise before call if unauthorized."""
if not call.context.user_id:
return await service_handler(call)
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index a0512178fdc47b..9c849bee22efb0 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -1,4 +1,5 @@
"""Template helper methods for rendering strings with Home Assistant data."""
+import asyncio
import base64
import collections.abc
from datetime import datetime, timedelta
@@ -6,9 +7,10 @@
import json
import logging
import math
+from operator import attrgetter
import random
import re
-from typing import Any, Iterable, List, Optional, Union
+from typing import Any, Generator, Iterable, List, Optional, Union
from urllib.parse import urlencode as urllib_urlencode
import weakref
@@ -35,6 +37,7 @@
from homeassistant.loader import bind_hass
from homeassistant.util import convert, dt as dt_util, location as loc_util
from homeassistant.util.async_ import run_callback_threadsafe
+from homeassistant.util.thread import ThreadWithException
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
@@ -58,6 +61,19 @@
_GROUP_DOMAIN_PREFIX = "group."
+_COLLECTABLE_STATE_ATTRIBUTES = {
+ "state",
+ "attributes",
+ "last_changed",
+ "last_updated",
+ "context",
+ "domain",
+ "object_id",
+ "name",
+}
+
+DEFAULT_RATE_LIMIT = timedelta(minutes=1)
+
@bind_hass
def attach(hass: HomeAssistantType, obj: Any) -> None:
@@ -163,6 +179,10 @@ def _true(arg: Any) -> bool:
return True
+def _false(arg: Any) -> bool:
+ return False
+
+
class RenderInfo:
"""Holds information about a template render."""
@@ -171,23 +191,31 @@ def __init__(self, template):
self.template = template
# Will be set sensibly once frozen.
self.filter_lifecycle = _true
+ self.filter = _true
self._result = None
self.is_static = False
self.exception = None
self.all_states = False
+ self.all_states_lifecycle = False
self.domains = set()
+ self.domains_lifecycle = set()
self.entities = set()
+ self.rate_limit = None
- def filter(self, entity_id: str) -> bool:
- """Template should re-render if the state changes."""
- return entity_id in self.entities
+ def __repr__(self) -> str:
+ """Representation of RenderInfo."""
+ return f""
- def _filter_lifecycle(self, entity_id: str) -> bool:
- """Template should re-render if the state changes."""
+ def _filter_domains_and_entities(self, entity_id: str) -> bool:
+ """Template should re-render if the entity state changes when we match specific domains or entities."""
return (
split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities
)
+ def _filter_lifecycle_domains(self, entity_id: str) -> bool:
+ """Template should re-render if the entity is added or removed with domains watched."""
+ return split_entity_id(entity_id)[0] in self.domains_lifecycle
+
def result(self) -> str:
"""Results of the template computation."""
if self.exception is not None:
@@ -196,21 +224,40 @@ def result(self) -> str:
def _freeze_static(self) -> None:
self.is_static = True
- self.entities = frozenset(self.entities)
- self.domains = frozenset(self.domains)
+ self._freeze_sets()
self.all_states = False
- def _freeze(self) -> None:
+ def _freeze_sets(self) -> None:
self.entities = frozenset(self.entities)
self.domains = frozenset(self.domains)
+ self.domains_lifecycle = frozenset(self.domains_lifecycle)
- if self.all_states or self.exception:
+ def _freeze(self) -> None:
+ self._freeze_sets()
+
+ if self.rate_limit is None and (
+ self.domains or self.domains_lifecycle or self.all_states or self.exception
+ ):
+ # If the template accesses all states or an entire
+ # domain, and no rate limit is set, we use the default.
+ self.rate_limit = DEFAULT_RATE_LIMIT
+
+ if self.exception:
+ return
+
+ if not self.all_states_lifecycle:
+ if self.domains_lifecycle:
+ self.filter_lifecycle = self._filter_lifecycle_domains
+ else:
+ self.filter_lifecycle = _false
+
+ if self.all_states:
return
- if not self.domains:
- self.filter_lifecycle = self.filter
+ if self.entities or self.domains:
+ self.filter = self._filter_domains_and_entities
else:
- self.filter_lifecycle = self._filter_lifecycle
+ self.filter = _false
class Template:
@@ -243,7 +290,7 @@ def ensure_valid(self):
try:
self._compiled_code = self._env.compile(self.template)
- except jinja2.exceptions.TemplateSyntaxError as err:
+ except jinja2.TemplateError as err:
raise TemplateError(err) from err
def extract_entities(
@@ -286,6 +333,54 @@ def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str
except jinja2.TemplateError as err:
raise TemplateError(err) from err
+ async def async_render_will_timeout(
+ self, timeout: float, variables: TemplateVarsType = None, **kwargs: Any
+ ) -> bool:
+ """Check to see if rendering a template will timeout during render.
+
+ This is intended to check for expensive templates
+ that will make the system unstable. The template
+ is rendered in the executor to ensure it does not
+ tie up the event loop.
+
+ This function is not a security control and is only
+ intended to be used as a safety check when testing
+ templates.
+
+ This method must be run in the event loop.
+ """
+ assert self.hass
+
+ if self.is_static:
+ return False
+
+ compiled = self._compiled or self._ensure_compiled()
+
+ if variables is not None:
+ kwargs.update(variables)
+
+ finish_event = asyncio.Event()
+
+ def _render_template():
+ try:
+ compiled.render(kwargs)
+ except TimeoutError:
+ pass
+ finally:
+ run_callback_threadsafe(self.hass.loop, finish_event.set)
+
+ try:
+ template_render_thread = ThreadWithException(target=_render_template)
+ template_render_thread.start()
+ await asyncio.wait_for(finish_event.wait(), timeout=timeout)
+ except asyncio.TimeoutError:
+ template_render_thread.raise_exc(TimeoutError)
+ return True
+ finally:
+ template_render_thread.join()
+
+ return False
+
@callback
def async_render_to_info(
self, variables: TemplateVarsType = None, **kwargs: Any
@@ -404,9 +499,7 @@ def __init__(self, hass):
def __getattr__(self, name):
"""Return the domain state."""
if "." in name:
- if not valid_entity_id(name):
- raise TemplateError(f"Invalid entity ID '{name}'")
- return _get_state(self._hass, name)
+ return _get_state_if_valid(self._hass, name)
if name in _RESERVED_NAMES:
return None
@@ -416,25 +509,29 @@ def __getattr__(self, name):
return DomainStates(self._hass, name)
+ # Jinja will try __getitem__ first and it avoids the need
+ # to call is_safe_attribute
+ __getitem__ = __getattr__
+
def _collect_all(self) -> None:
render_info = self._hass.data.get(_RENDER_INFO)
if render_info is not None:
render_info.all_states = True
+ def _collect_all_lifecycle(self) -> None:
+ render_info = self._hass.data.get(_RENDER_INFO)
+ if render_info is not None:
+ render_info.all_states_lifecycle = True
+
def __iter__(self):
"""Return all states."""
self._collect_all()
- return iter(
- _wrap_state(self._hass, state)
- for state in sorted(
- self._hass.states.async_all(), key=lambda state: state.entity_id
- )
- )
+ return _state_generator(self._hass, None)
def __len__(self) -> int:
"""Return number of states."""
- self._collect_all()
- return len(self._hass.states.async_entity_ids())
+ self._collect_all_lifecycle()
+ return self._hass.states.async_entity_ids_count()
def __call__(self, entity_id):
"""Return the states."""
@@ -456,33 +553,31 @@ def __init__(self, hass, domain):
def __getattr__(self, name):
"""Return the states."""
- entity_id = f"{self._domain}.{name}"
- if not valid_entity_id(entity_id):
- raise TemplateError(f"Invalid entity ID '{entity_id}'")
- return _get_state(self._hass, entity_id)
+ return _get_state_if_valid(self._hass, f"{self._domain}.{name}")
+
+ # Jinja will try __getitem__ first and it avoids the need
+ # to call is_safe_attribute
+ __getitem__ = __getattr__
def _collect_domain(self) -> None:
entity_collect = self._hass.data.get(_RENDER_INFO)
if entity_collect is not None:
entity_collect.domains.add(self._domain)
+ def _collect_domain_lifecycle(self) -> None:
+ entity_collect = self._hass.data.get(_RENDER_INFO)
+ if entity_collect is not None:
+ entity_collect.domains_lifecycle.add(self._domain)
+
def __iter__(self):
"""Return the iteration over all the states."""
self._collect_domain()
- return iter(
- sorted(
- (
- _wrap_state(self._hass, state)
- for state in self._hass.states.async_all(self._domain)
- ),
- key=lambda state: state.entity_id,
- )
- )
+ return _state_generator(self._hass, self._domain)
def __len__(self) -> int:
"""Return number of states."""
- self._collect_domain()
- return len(self._hass.states.async_entity_ids(self._domain))
+ self._collect_domain_lifecycle()
+ return self._hass.states.async_entity_ids_count(self._domain)
def __repr__(self) -> str:
"""Representation of Domain States."""
@@ -492,53 +587,106 @@ def __repr__(self) -> str:
class TemplateState(State):
"""Class to represent a state object in a template."""
+ __slots__ = ("_hass", "_state", "_collect")
+
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
- def __init__(self, hass, state):
+ def __init__(self, hass, state, collect=True):
"""Initialize template state."""
self._hass = hass
self._state = state
+ self._collect = collect
+
+ def _collect_state(self):
+ if self._collect and _RENDER_INFO in self._hass.data:
+ self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
+
+ # Jinja will try __getitem__ first and it avoids the need
+ # to call is_safe_attribute
+ def __getitem__(self, item):
+ """Return a property as an attribute for jinja."""
+ if item in _COLLECTABLE_STATE_ATTRIBUTES:
+ # _collect_state inlined here for performance
+ if self._collect and _RENDER_INFO in self._hass.data:
+ self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
+ return getattr(self._state, item)
+ if item == "entity_id":
+ return self._state.entity_id
+ if item == "state_with_unit":
+ return self.state_with_unit
+ raise KeyError
+
+ @property
+ def entity_id(self):
+ """Wrap State.entity_id.
+
+ Intentionally does not collect state
+ """
+ return self._state.entity_id
+
+ @property
+ def state(self):
+ """Wrap State.state."""
+ self._collect_state()
+ return self._state.state
+
+ @property
+ def attributes(self):
+ """Wrap State.attributes."""
+ self._collect_state()
+ return self._state.attributes
+
+ @property
+ def last_changed(self):
+ """Wrap State.last_changed."""
+ self._collect_state()
+ return self._state.last_changed
+
+ @property
+ def last_updated(self):
+ """Wrap State.last_updated."""
+ self._collect_state()
+ return self._state.last_updated
- def _access_state(self):
- state = object.__getattribute__(self, "_state")
- hass = object.__getattribute__(self, "_hass")
- _collect_state(hass, state.entity_id)
- return state
+ @property
+ def context(self):
+ """Wrap State.context."""
+ self._collect_state()
+ return self._state.context
+
+ @property
+ def domain(self):
+ """Wrap State.domain."""
+ self._collect_state()
+ return self._state.domain
+
+ @property
+ def object_id(self):
+ """Wrap State.object_id."""
+ self._collect_state()
+ return self._state.object_id
+
+ @property
+ def name(self):
+ """Wrap State.name."""
+ self._collect_state()
+ return self._state.name
@property
def state_with_unit(self) -> str:
"""Return the state concatenated with the unit if available."""
- state = object.__getattribute__(self, "_access_state")()
- unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- if unit is None:
- return state.state
- return f"{state.state} {unit}"
+ self._collect_state()
+ unit = self._state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ return f"{self._state.state} {unit}" if unit else self._state.state
def __eq__(self, other: Any) -> bool:
"""Ensure we collect on equality check."""
- state = object.__getattribute__(self, "_state")
- hass = object.__getattribute__(self, "_hass")
- _collect_state(hass, state.entity_id)
- return super().__eq__(other)
-
- def __getattribute__(self, name):
- """Return an attribute of the state."""
- # This one doesn't count as an access of the state
- # since we either found it by looking direct for the ID
- # or got it off an iterator.
- if name == "entity_id" or name in object.__dict__:
- state = object.__getattribute__(self, "_state")
- return getattr(state, name)
- if name in TemplateState.__dict__:
- return object.__getattribute__(self, name)
- state = object.__getattribute__(self, "_access_state")()
- return getattr(state, name)
+ self._collect_state()
+ return self._state.__eq__(other)
def __repr__(self) -> str:
"""Representation of Template State."""
- state = object.__getattribute__(self, "_access_state")()
- rep = state.__repr__()
- return f""
def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
@@ -547,21 +695,34 @@ def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
entity_collect.entities.add(entity_id)
-def _wrap_state(
- hass: HomeAssistantType, state: Optional[State]
+def _state_generator(hass: HomeAssistantType, domain: Optional[str]) -> Generator:
+ """State generator for a domain or all states."""
+ for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")):
+ yield TemplateState(hass, state, collect=False)
+
+
+def _get_state_if_valid(
+ hass: HomeAssistantType, entity_id: str
) -> Optional[TemplateState]:
- """Wrap a state."""
- return None if state is None else TemplateState(hass, state)
+ state = hass.states.get(entity_id)
+ if state is None and not valid_entity_id(entity_id):
+ raise TemplateError(f"Invalid entity ID '{entity_id}'") # type: ignore
+ return _get_template_state_from_state(hass, entity_id, state)
def _get_state(hass: HomeAssistantType, entity_id: str) -> Optional[TemplateState]:
- state = hass.states.get(entity_id)
+ return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id))
+
+
+def _get_template_state_from_state(
+ hass: HomeAssistantType, entity_id: str, state: Optional[State]
+) -> Optional[TemplateState]:
if state is None:
# Only need to collect if none, if not none collect first actual
# access to the state properties in the state wrapper.
_collect_state(hass, entity_id)
return None
- return _wrap_state(hass, state)
+ return TemplateState(hass, state)
def _resolve_state(
@@ -917,7 +1078,7 @@ def strptime(string, fmt):
"""Parse a time string to datetime."""
try:
return datetime.strptime(string, fmt)
- except (ValueError, AttributeError):
+ except (ValueError, AttributeError, TypeError):
return string
@@ -1137,12 +1298,12 @@ def is_safe_callable(self, obj):
def is_safe_attribute(self, obj, attr, value):
"""Test if attribute is safe."""
+ if isinstance(obj, (AllStates, DomainStates, TemplateState)):
+ return not attr[0] == "_"
+
if isinstance(obj, Namespace):
return True
- if isinstance(obj, (AllStates, DomainStates, TemplateState)):
- return not attr.startswith("_")
-
return super().is_safe_attribute(obj, attr, value)
def compile(self, source, name=None, filename=None, raw=False, defer_init=False):
diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py
index 44e10243598d96..a43c15dbfbd2d5 100644
--- a/homeassistant/helpers/update_coordinator.py
+++ b/homeassistant/helpers/update_coordinator.py
@@ -213,9 +213,14 @@ async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
- self.coordinator.async_add_listener(self.async_write_ha_state)
+ self.coordinator.async_add_listener(self._handle_coordinator_update)
)
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ self.async_write_ha_state()
+
async def async_update(self) -> None:
"""Update the entity.
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index afbec0dd3d09af..cdda546a4e1362 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -13,11 +13,11 @@ defusedxml==0.6.0
distro==1.5.0
emoji==0.5.4
hass-nabucasa==0.37.0
-home-assistant-frontend==20200909.0
+home-assistant-frontend==20201001.1
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
netdisco==2.8.2
-paho-mqtt==1.5.0
+paho-mqtt==1.5.1
pillow==7.2.0
pip>=8.0.3
python-slugify==4.0.1
@@ -27,7 +27,7 @@ requests==2.24.0
ruamel.yaml==0.15.100
sqlalchemy==1.3.19
voluptuous-serialize==2.4.0
-voluptuous==0.11.7
+voluptuous==0.12.0
yarl==1.4.2
zeroconf==0.28.5
@@ -39,6 +39,10 @@ urllib3>=1.24.3
# Constrain httplib2 to protect against CVE-2020-11078
httplib2>=0.18.0
+# gRPC 1.32+ currently causes issues on ARMv7, see:
+# https://github.com/home-assistant/core/issues/40148
+grpcio==1.31.0
+
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index 818e28479fba57..4060b0410d6601 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -77,7 +77,7 @@ async def _async_process_dependencies(
if dep not in hass.config.components
}
- after_dependencies_tasks = dict()
+ after_dependencies_tasks = {}
to_be_loaded = hass.data.get(DATA_SETUP_DONE, {})
for dep in integration.after_dependencies:
if (
diff --git a/homeassistant/strings.json b/homeassistant/strings.json
index 05bc2e3c247917..9449c086023c5a 100644
--- a/homeassistant/strings.json
+++ b/homeassistant/strings.json
@@ -25,6 +25,7 @@
"confirm_setup": "Do you want to start set up?"
},
"data": {
+ "name": "Name",
"email": "Email",
"username": "Username",
"password": "Password",
@@ -34,7 +35,13 @@
"url": "URL",
"usb_path": "USB Device Path",
"access_token": "Access Token",
- "api_key": "API Key"
+ "api_key": "API Key",
+ "api_token": "API Token",
+ "ssl": "Uses an SSL certificate",
+ "verify_ssl": "Verify SSL certificate",
+ "longitude": "Longitude",
+ "latitude": "Latitude",
+ "pin": "PIN Code"
},
"create_entry": {
"authenticated": "Successfully authenticated"
@@ -51,10 +58,12 @@
"already_configured_account": "Account is already configured",
"already_configured_service": "Service is already configured",
"already_configured_device": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"no_devices_found": "No devices found on the network",
"oauth2_missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth2_authorize_url_timeout": "Timeout generating authorize URL.",
- "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
+ "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+ "reauth_successful": "Re-authentication was successful"
}
}
}
diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py
index 629928f43d7528..db40412cbf2f57 100644
--- a/homeassistant/util/logging.py
+++ b/homeassistant/util/logging.py
@@ -98,7 +98,7 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None:
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
- # https://github.com/home-assistant/home-assistant/issues/24982
+ # https://github.com/home-assistant/core/issues/24982
module_name = __name__
# Do not print the wrapper in the traceback
diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py
index e5654e6f8c6a09..bf61c67172ad42 100644
--- a/homeassistant/util/thread.py
+++ b/homeassistant/util/thread.py
@@ -1,4 +1,6 @@
"""Threading util helpers."""
+import ctypes
+import inspect
import sys
import threading
from typing import Any
@@ -24,3 +26,34 @@ def run(*args: Any, **kwargs: Any) -> None:
sys.excepthook(*sys.exc_info())
threading.Thread.run = run # type: ignore
+
+
+def _async_raise(tid: int, exctype: Any) -> None:
+ """Raise an exception in the threads with id tid."""
+ if not inspect.isclass(exctype):
+ raise TypeError("Only types can be raised (not instances)")
+
+ c_tid = ctypes.c_long(tid)
+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(c_tid, ctypes.py_object(exctype))
+
+ if res == 1:
+ return
+
+ # "if it returns a number greater than one, you're in trouble,
+ # and you should call it again with exc=NULL to revert the effect"
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(c_tid, None)
+ raise SystemError("PyThreadState_SetAsyncExc failed")
+
+
+class ThreadWithException(threading.Thread):
+ """A thread class that supports raising exception in the thread from another thread.
+
+ Based on
+ https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread/49877671
+
+ """
+
+ def raise_exc(self, exctype: Any) -> None:
+ """Raise the given exception type in the context of this thread."""
+ assert self.ident
+ _async_raise(self.ident, exctype)
diff --git a/pylintrc b/pylintrc
deleted file mode 100644
index 86f3d4caea07bf..00000000000000
--- a/pylintrc
+++ /dev/null
@@ -1,65 +0,0 @@
-[MASTER]
-ignore=tests
-# Use a conservative default here; 2 should speed up most setups and not hurt
-# any too bad. Override on command line as appropriate.
-# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
-#jobs=2
-load-plugins=pylint_strict_informational
-persistent=no
-extension-pkg-whitelist=ciso8601,cv2
-
-[BASIC]
-good-names=id,i,j,k,ex,Run,_,fp,T,ev
-
-[MESSAGES CONTROL]
-# Reasons disabled:
-# format - handled by black
-# locally-disabled - it spams too much
-# duplicate-code - unavoidable
-# cyclic-import - doesn't test if both import on load
-# abstract-class-little-used - prevents from setting right foundation
-# unused-argument - generic callbacks and setup methods create a lot of warnings
-# too-many-* - are not enforced for the sake of readability
-# too-few-* - same as too-many-*
-# abstract-method - with intro of async there are always methods missing
-# inconsistent-return-statements - doesn't handle raise
-# too-many-ancestors - it's too strict.
-# wrong-import-order - isort guards this
-disable=
- format,
- abstract-class-little-used,
- abstract-method,
- cyclic-import,
- duplicate-code,
- inconsistent-return-statements,
- locally-disabled,
- not-context-manager,
- too-few-public-methods,
- too-many-ancestors,
- too-many-arguments,
- too-many-branches,
- too-many-instance-attributes,
- too-many-lines,
- too-many-locals,
- too-many-public-methods,
- too-many-return-statements,
- too-many-statements,
- too-many-boolean-expressions,
- unused-argument,
- wrong-import-order
-# enable useless-suppression temporarily every now and then to clean them up
-enable=
- use-symbolic-message-instead
-
-[REPORTS]
-score=no
-
-[TYPECHECK]
-# For attrs
-ignored-classes=_CountingAttr
-
-[FORMAT]
-expected-line-ending-format=LF
-
-[EXCEPTIONS]
-overgeneral-exceptions=BaseException,Exception,HomeAssistantError
diff --git a/pyproject.toml b/pyproject.toml
index 7c0c5eeb4337f9..0f416d9e0141cf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,117 @@
[tool.black]
target-version = ["py37", "py38"]
exclude = 'generated'
+
+[tool.isort]
+# https://github.com/PyCQA/isort/wiki/isort-Settings
+profile = "black"
+# will group `import x` and `from x import` of the same module.
+force_sort_within_sections = true
+known_first_party = [
+ "homeassistant",
+ "tests",
+]
+forced_separate = [
+ "tests",
+]
+combine_as_imports = true
+
+[tool.pylint.MASTER]
+ignore = [
+ "tests",
+]
+# Use a conservative default here; 2 should speed up most setups and not hurt
+# any too bad. Override on command line as appropriate.
+# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
+#jobs = 2
+load-plugins = [
+ "pylint_strict_informational",
+]
+persistent = false
+extension-pkg-whitelist = [
+ "ciso8601",
+ "cv2",
+]
+
+[tool.pylint.BASIC]
+good-names = [
+ "_",
+ "ev",
+ "ex",
+ "fp",
+ "i",
+ "id",
+ "j",
+ "k",
+ "Run",
+ "T",
+]
+
+[tool.pylint."MESSAGES CONTROL"]
+# Reasons disabled:
+# format - handled by black
+# locally-disabled - it spams too much
+# duplicate-code - unavoidable
+# cyclic-import - doesn't test if both import on load
+# abstract-class-little-used - prevents from setting right foundation
+# unused-argument - generic callbacks and setup methods create a lot of warnings
+# too-many-* - are not enforced for the sake of readability
+# too-few-* - same as too-many-*
+# abstract-method - with intro of async there are always methods missing
+# inconsistent-return-statements - doesn't handle raise
+# too-many-ancestors - it's too strict.
+# wrong-import-order - isort guards this
+disable = [
+ "format",
+ "abstract-class-little-used",
+ "abstract-method",
+ "cyclic-import",
+ "duplicate-code",
+ "inconsistent-return-statements",
+ "locally-disabled",
+ "not-context-manager",
+ "too-few-public-methods",
+ "too-many-ancestors",
+ "too-many-arguments",
+ "too-many-branches",
+ "too-many-instance-attributes",
+ "too-many-lines",
+ "too-many-locals",
+ "too-many-public-methods",
+ "too-many-return-statements",
+ "too-many-statements",
+ "too-many-boolean-expressions",
+ "unused-argument",
+ "wrong-import-order",
+]
+enable = [
+ #"useless-suppression", # temporarily every now and then to clean them up
+ "use-symbolic-message-instead",
+]
+
+[tool.pylint.REPORTS]
+score = false
+
+[tool.pylint.TYPECHECK]
+ignored-classes = [
+ "_CountingAttr", # for attrs
+]
+
+[tool.pylint.FORMAT]
+expected-line-ending-format = "LF"
+
+[tool.pylint.EXCEPTIONS]
+overgeneral-exceptions = [
+ "BaseException",
+ "Exception",
+ "HomeAssistantError",
+]
+
+[tool.pytest.ini_options]
+testpaths = [
+ "tests",
+]
+norecursedirs = [
+ ".git",
+ "testing_config",
+]
diff --git a/requirements.txt b/requirements.txt
index baa48241a06495..91f09a543905c5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,6 +18,6 @@ pytz>=2020.1
pyyaml==5.3.1
requests==2.24.0
ruamel.yaml==0.15.100
-voluptuous==0.11.7
+voluptuous==0.12.0
voluptuous-serialize==2.4.0
yarl==1.4.2
diff --git a/requirements_all.txt b/requirements_all.txt
index a2256a6ea825e9..0532e7be4061ee 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -26,7 +26,7 @@ Mastodon.py==1.5.1
OPi.GPIO==0.4.0
# homeassistant.components.plugwise
-Plugwise_Smile==1.4.0
+Plugwise_Smile==1.5.1
# homeassistant.components.essent
PyEssent==0.13
@@ -70,7 +70,7 @@ PyTurboJPEG==1.4.0
PyViCare==0.2.0
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.13.2
+PyXiaomiGateway==0.13.3
# homeassistant.components.bmp280
# homeassistant.components.mcp23017
@@ -102,7 +102,7 @@ YesssSMS==0.4.1
abodepy==1.1.0
# homeassistant.components.accuweather
-accuweather==0.0.10
+accuweather==0.0.11
# homeassistant.components.mcp23017
adafruit-blinka==3.9.0
@@ -178,7 +178,7 @@ aioguardian==1.0.1
aioharmony==0.2.6
# homeassistant.components.homekit_controller
-aiohomekit==0.2.49
+aiohomekit==0.2.53
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -221,7 +221,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
-aioshelly==0.3.1
+aioshelly==0.3.4
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -233,7 +233,7 @@ aiounifi==23
aioymaps==1.1.0
# homeassistant.components.airly
-airly==0.0.2
+airly==1.0.0
# homeassistant.components.aladdin_connect
aladdin_connect==0.3
@@ -248,7 +248,7 @@ ambiclimate==0.2.1
amcrest==1.7.0
# homeassistant.components.androidtv
-androidtv[async]==0.0.49
+androidtv[async]==0.0.50
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@@ -263,7 +263,7 @@ apcaccess==0.0.13
apns2==0.3.0
# homeassistant.components.apprise
-apprise==0.8.8
+apprise==0.8.9
# homeassistant.components.aprs
aprslib==0.6.46
@@ -309,13 +309,13 @@ av==8.0.2
avri-api==0.1.7
# homeassistant.components.axis
-axis==35
+axis==37
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
# homeassistant.components.azure_service_bus
-azure-servicebus==0.50.1
+azure-servicebus==0.50.3
# homeassistant.components.baidu
baidu-aip==1.6.6
@@ -339,7 +339,7 @@ beautifulsoup4==4.9.1
# beewi_smartclim==0.0.7
# homeassistant.components.zha
-bellows==0.20.1
+bellows==0.20.3
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.7
@@ -383,7 +383,7 @@ boto3==1.9.252
bravia-tv==1.0.6
# homeassistant.components.broadlink
-broadlink==0.14.1
+broadlink==0.15.0
# homeassistant.components.brother
brother==0.1.17
@@ -481,10 +481,10 @@ defusedxml==0.6.0
deluge-client==1.7.1
# homeassistant.components.denonavr
-denonavr==0.9.4
+denonavr==0.9.5
# homeassistant.components.devolo_home_control
-devolo-home-control-api==0.13.0
+devolo-home-control-api==0.15.0
# homeassistant.components.directv
directv==0.3.0
@@ -511,7 +511,7 @@ dovado==0.4.1
dsmr_parser==0.18
# homeassistant.components.dwd_weather_warnings
-dwdwfsapi==1.0.2
+dwdwfsapi==1.0.3
# homeassistant.components.dweet
dweepy==0.3.0
@@ -668,8 +668,11 @@ glances_api==0.2.0
# homeassistant.components.gntp
gntp==1.0.3
+# homeassistant.components.goalzero
+goalzero==0.1.4
+
# homeassistant.components.gogogate2
-gogogate2-api==2.0.1
+gogogate2-api==2.0.3
# homeassistant.components.google
google-api-python-client==1.6.4
@@ -717,11 +720,17 @@ ha-philipsjs==0.0.8
habitipy==0.2.0
# homeassistant.components.hangouts
-hangups==0.4.10
+hangups==0.4.11
# homeassistant.components.cloud
hass-nabucasa==0.37.0
+# homeassistant.components.splunk
+hass_splunk==0.1.1
+
+# homeassistant.components.tasmota
+hatasmota==0.0.10
+
# homeassistant.components.jewish_calendar
hdate==0.9.5
@@ -747,7 +756,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20200909.0
+home-assistant-frontend==20201001.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -771,6 +780,9 @@ huawei-lte-api==1.4.12
# homeassistant.components.hydrawise
hydrawiser==0.2
+# homeassistant.components.hyperion
+hyperion-py==0.3.0
+
# homeassistant.components.bh1750
# homeassistant.components.bme280
# homeassistant.components.htu21d
@@ -1006,6 +1018,9 @@ oauth2client==4.0.0
# homeassistant.components.oem
oemthermostat==1.1
+# homeassistant.components.omnilogic
+omnilogic==0.4.0
+
# homeassistant.components.onkyo
onkyo-eiscp==1.2.7
@@ -1047,7 +1062,7 @@ ovoenergy==1.1.7
# homeassistant.components.mqtt
# homeassistant.components.shiftr
-paho-mqtt==1.5.0
+paho-mqtt==1.5.1
# homeassistant.components.panasonic_bluray
panacotta==0.1
@@ -1098,13 +1113,13 @@ pillow==7.2.0
pizzapi==0.0.3
# homeassistant.components.plex
-plexapi==4.1.0
+plexapi==4.1.1
# homeassistant.components.plex
plexauth==0.0.5
# homeassistant.components.plex
-plexwebsocket==0.0.11
+plexwebsocket==0.0.12
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1174,7 +1189,7 @@ py-cpuinfo==7.0.0
py-melissa-climate==2.1.4
# homeassistant.components.nightscout
-py-nightscout==1.2.1
+py-nightscout==1.2.2
# homeassistant.components.schluter
py-schluter==0.1.7
@@ -1185,6 +1200,9 @@ py-synology==0.2.0
# homeassistant.components.seventeentrack
py17track==2.2.2
+# homeassistant.components.hdmi_cec
+pyCEC==0.4.14
+
# homeassistant.components.control4
pyControl4==0.0.6
@@ -1196,13 +1214,13 @@ pyHS100==0.3.5.1
pyMetno==0.8.1
# homeassistant.components.rfxtrx
-pyRFXtrx==0.25
+pyRFXtrx==0.26
# homeassistant.components.switchmate
# pySwitchmate==0.4.6
# homeassistant.components.tibber
-pyTibber==0.15.2
+pyTibber==0.15.3
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1223,7 +1241,7 @@ pyaehw4a1==0.3.9
pyaftership==0.1.2
# homeassistant.components.airvisual
-pyairvisual==4.4.0
+pyairvisual==5.0.2
# homeassistant.components.almond
pyalmond==0.0.2
@@ -1232,7 +1250,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
-pyatag==0.3.3.4
+pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.0.0
@@ -1265,7 +1283,7 @@ pycfdns==0.0.1
pychannels==1.0.0
# homeassistant.components.cast
-pychromecast==7.2.1
+pychromecast==7.5.0
# homeassistant.components.cmus
pycmus==0.1.1
@@ -1277,7 +1295,7 @@ pycocotools==2.0.1
pycomfoconnect==0.3
# homeassistant.components.coolmaster
-pycoolmasternet-async==0.1.1
+pycoolmasternet-async==0.1.2
# homeassistant.components.avri
pycountry==19.8.18
@@ -1295,7 +1313,7 @@ pydaikin==2.3.1
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==72
+pydeconz==73
# homeassistant.components.delijn
pydelijn==0.6.1
@@ -1377,7 +1395,7 @@ pygtfs==0.1.5
pygti==0.6.0
# homeassistant.components.version
-pyhaversion==3.3.0
+pyhaversion==3.4.2
# homeassistant.components.heos
pyheos==0.6.0
@@ -1386,10 +1404,10 @@ pyheos==0.6.0
pyhik==0.2.7
# homeassistant.components.hive
-pyhiveapi==0.2.20.1
+pyhiveapi==0.2.20.2
# homeassistant.components.homematic
-pyhomematic==0.1.68
+pyhomematic==0.1.70
# homeassistant.components.homeworks
pyhomeworks==0.0.6
@@ -1401,7 +1419,7 @@ pyialarm==0.3
pyicloud==0.9.7
# homeassistant.components.insteon
-pyinsteon==1.0.7
+pyinsteon==1.0.8
# homeassistant.components.intesishome
pyintesishome==1.7.5
@@ -1431,7 +1449,7 @@ pyitachip2ir==0.0.7
pykira==0.1.1
# homeassistant.components.kodi
-pykodi==0.1.2
+pykodi==0.2.1
# homeassistant.components.kwb
pykwb==0.0.8
@@ -1458,7 +1476,7 @@ pylitejet==0.1
pyloopenergy==0.2.1
# homeassistant.components.lutron_caseta
-pylutron-caseta==0.6.1
+pylutron-caseta==0.7.0
# homeassistant.components.lutron
pylutron==0.2.5
@@ -1467,7 +1485,7 @@ pylutron==0.2.5
pymailgunner==1.4
# homeassistant.components.firmata
-pymata-express==1.13
+pymata-express==1.19
# homeassistant.components.mediaroom
pymediaroom==0.6.4.1
@@ -1521,7 +1539,7 @@ pynuki==1.3.8
pynut2==2.1.2
# homeassistant.components.nws
-pynws==1.2.1
+pynws==1.3.0
# homeassistant.components.nx584
pynx584==0.5
@@ -1565,7 +1583,7 @@ pyownet==0.10.0.post1
pypca==0.0.7
# homeassistant.components.lcn
-pypck==0.6.4
+pypck==0.7.2
# homeassistant.components.pjlink
pypjlink2==1.2.1
@@ -1573,6 +1591,9 @@ pypjlink2==1.2.1
# homeassistant.components.point
pypoint==1.1.2
+# homeassistant.components.profiler
+pyprof2calltree==1.4.5
+
# homeassistant.components.ps4
pyps4-2ndscreen==1.1.1
@@ -1595,7 +1616,7 @@ pyrecswitch==1.0.2
pyrepetier==3.0.5
# homeassistant.components.risco
-pyrisco==0.3.0
+pyrisco==0.3.1
# homeassistant.components.sabnzbd
pysabnzbd==1.1.0
@@ -1638,7 +1659,7 @@ pysmappee==0.2.13
pysmartapp==0.3.2
# homeassistant.components.smartthings
-pysmartthings==0.7.3
+pysmartthings==0.7.4
# homeassistant.components.smarty
pysmarty==0.8
@@ -1653,7 +1674,7 @@ pysnmp==4.4.12
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.33
+pysonos==0.0.34
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1746,7 +1767,7 @@ python-nest==4.1.0
python-nmap==0.6.1
# homeassistant.components.ozw
-python-openzwave-mqtt==1.0.5
+python-openzwave-mqtt==1.2.0
# homeassistant.components.qbittorrent
python-qbittorrent==0.4.1
@@ -1776,7 +1797,7 @@ python-telnet-vlc==1.0.4
python-twitch-client==0.6.0
# homeassistant.components.velbus
-python-velbus==2.0.44
+python-velbus==2.0.46
# homeassistant.components.vlc
python-vlc==1.1.2
@@ -1825,19 +1846,19 @@ pyuptimerobot==0.0.5
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.3.9
+pyvera==0.3.10
# homeassistant.components.versasense
pyversasense==0.0.6
# homeassistant.components.vesync
-pyvesync==1.1.0
+pyvesync==1.2.0
# homeassistant.components.vizio
pyvizio==0.1.56
# homeassistant.components.velux
-pyvlx==0.2.16
+pyvlx==0.2.17
# homeassistant.components.volumio
pyvolumio==0.1.2
@@ -1846,7 +1867,7 @@ pyvolumio==0.1.2
pywebpush==1.9.2
# homeassistant.components.wemo
-pywemo==0.4.46
+pywemo==0.5.0
# homeassistant.components.wilight
pywilight==0.0.65
@@ -1923,6 +1944,9 @@ roonapi==0.0.21
# homeassistant.components.rova
rova==0.1.0
+# homeassistant.components.rpi_power
+rpi-bad-power==0.0.3
+
# homeassistant.components.rpi_rf
# rpi-rf==0.9.7
@@ -1964,7 +1988,7 @@ sense-hat==2.2.0
sense_energy==0.8.0
# homeassistant.components.sentry
-sentry-sdk==0.17.4
+sentry-sdk==0.18.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -1982,7 +2006,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==9.3.0
+simplisafe-python==9.4.1
# homeassistant.components.sisyphus
sisyphus-control==2.2.1
@@ -2017,7 +2041,7 @@ smarthab==0.21
smhi-pkg==1.0.13
# homeassistant.components.snapcast
-snapcast==2.0.10
+snapcast==2.1.1
# homeassistant.components.socialblade
socialbladeclient==0.5
@@ -2029,7 +2053,7 @@ solaredge-local==0.2.0
solaredge==0.0.2
# homeassistant.components.solax
-solax==0.2.3
+solax==0.2.4
# homeassistant.components.honeywell
somecomfort==0.5.2
@@ -2038,7 +2062,7 @@ somecomfort==0.5.2
somfy-mylink-synergy==1.0.6
# homeassistant.components.sonarr
-sonarr==0.2.3
+sonarr==0.3.0
# homeassistant.components.marytts
speak2mary==1.4.0
@@ -2053,7 +2077,7 @@ spiderpy==1.3.1
spotcrime==1.0.4
# homeassistant.components.spotify
-spotipy==2.14.0
+spotipy==2.16.0
# homeassistant.components.recorder
# homeassistant.components.sql
@@ -2090,7 +2114,7 @@ sucks==0.9.4
sunwatcher==0.2.1
# homeassistant.components.surepetcare
-surepy==0.2.5
+surepy==0.2.6
# homeassistant.components.swiss_hydrological_data
swisshydrodata==0.0.3
@@ -2247,7 +2271,7 @@ withings-api==2.1.6
wled==0.4.4
# homeassistant.components.wolflink
-wolf_smartset==0.1.4
+wolf_smartset==0.1.6
# homeassistant.components.xbee
xbee-helper==0.0.7
@@ -2259,7 +2283,7 @@ xboxapi==2.0.1
xfinity-gateway==0.0.4
# homeassistant.components.knx
-xknx==0.13.0
+xknx==0.15.0
# homeassistant.components.bluesound
# homeassistant.components.rest
@@ -2281,7 +2305,7 @@ yeelight==0.5.3
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2020.07.28
+youtube_dl==2020.09.20
# homeassistant.components.zengge
zengge==0.2
@@ -2290,7 +2314,7 @@ zengge==0.2
zeroconf==0.28.5
# homeassistant.components.zha
-zha-quirks==0.0.44
+zha-quirks==0.0.45
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2302,7 +2326,7 @@ ziggo-mediabox-xl==1.1.0
zigpy-cc==0.5.2
# homeassistant.components.zha
-zigpy-deconz==0.9.2
+zigpy-deconz==0.10.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
@@ -2311,10 +2335,10 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.6.2
# homeassistant.components.zha
-zigpy-znp==0.1.1
+zigpy-znp==0.2.1
# homeassistant.components.zha
-zigpy==0.23.2
+zigpy==0.25.0
# homeassistant.components.zoneminder
zm-py==0.4.0
diff --git a/requirements_test.txt b/requirements_test.txt
index e36837edf638ee..0fd681b62b2460 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -5,24 +5,24 @@
-c homeassistant/package_constraints.txt
-r requirements_test_pre_commit.txt
asynctest==0.13.0
-codecov==2.1.0
-coverage==5.2.1
+codecov==2.1.9
+coverage==5.3
jsonpickle==1.4.1
mock-open==1.4.0
-mypy==0.780
+mypy==0.782
pre-commit==2.7.1
pylint==2.6.0
astroid==2.4.2
pipdeptree==1.0.0
pylint-strict-informational==0.1
pytest-aiohttp==0.3.0
-pytest-cov==2.10.0
+pytest-cov==2.10.1
pytest-test-groups==1.0.3
-pytest-sugar==0.9.3
-pytest-timeout==1.3.4
-pytest-xdist==1.32.0
-pytest==5.4.3
+pytest-sugar==0.9.4
+pytest-timeout==1.4.2
+pytest-xdist==2.1.0
+pytest==6.0.2
requests_mock==1.8.0
-responses==0.10.6
+responses==0.12.0
stdlib-list==0.7.0
-tqdm==4.48.2
+tqdm==4.49.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f4acc7ae79c060..31ff656e2615a2 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -7,7 +7,7 @@
HAP-python==3.0.0
# homeassistant.components.plugwise
-Plugwise_Smile==1.4.0
+Plugwise_Smile==1.5.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -30,7 +30,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.4.0
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.13.2
+PyXiaomiGateway==0.13.3
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -45,11 +45,14 @@ YesssSMS==0.4.1
abodepy==1.1.0
# homeassistant.components.accuweather
-accuweather==0.0.10
+accuweather==0.0.11
# homeassistant.components.androidtv
adb-shell[async]==0.2.1
+# homeassistant.components.alarmdecoder
+adext==0.3
+
# homeassistant.components.adguard
adguardhome==0.4.2
@@ -103,7 +106,7 @@ aioguardian==1.0.1
aioharmony==0.2.6
# homeassistant.components.homekit_controller
-aiohomekit==0.2.49
+aiohomekit==0.2.53
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -131,7 +134,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
-aioshelly==0.3.1
+aioshelly==0.3.4
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -143,19 +146,19 @@ aiounifi==23
aioymaps==1.1.0
# homeassistant.components.airly
-airly==0.0.2
+airly==1.0.0
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
# homeassistant.components.androidtv
-androidtv[async]==0.0.49
+androidtv[async]==0.0.50
# homeassistant.components.apns
apns2==0.3.0
# homeassistant.components.apprise
-apprise==0.8.8
+apprise==0.8.9
# homeassistant.components.aprs
aprslib==0.6.46
@@ -174,7 +177,7 @@ av==8.0.2
avri-api==0.1.7
# homeassistant.components.axis
-axis==35
+axis==37
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -183,7 +186,7 @@ azure-eventhub==5.1.0
base36==0.1.1
# homeassistant.components.zha
-bellows==0.20.1
+bellows==0.20.3
# homeassistant.components.blebox
blebox_uniapi==1.3.2
@@ -201,7 +204,7 @@ bond-api==0.1.8
bravia-tv==1.0.6
# homeassistant.components.broadlink
-broadlink==0.14.1
+broadlink==0.15.0
# homeassistant.components.brother
brother==0.1.17
@@ -248,10 +251,10 @@ debugpy==1.0.0rc2
defusedxml==0.6.0
# homeassistant.components.denonavr
-denonavr==0.9.4
+denonavr==0.9.5
# homeassistant.components.devolo_home_control
-devolo-home-control-api==0.13.0
+devolo-home-control-api==0.15.0
# homeassistant.components.directv
directv==0.3.0
@@ -333,8 +336,11 @@ gios==0.1.4
# homeassistant.components.glances
glances_api==0.2.0
+# homeassistant.components.goalzero
+goalzero==0.1.4
+
# homeassistant.components.gogogate2
-gogogate2-api==2.0.1
+gogogate2-api==2.0.3
# homeassistant.components.google
google-api-python-client==1.6.4
@@ -349,11 +355,14 @@ griddypower==0.1.0
ha-ffmpeg==2.0
# homeassistant.components.hangouts
-hangups==0.4.10
+hangups==0.4.11
# homeassistant.components.cloud
hass-nabucasa==0.37.0
+# homeassistant.components.tasmota
+hatasmota==0.0.10
+
# homeassistant.components.jewish_calendar
hdate==0.9.5
@@ -370,7 +379,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20200909.0
+home-assistant-frontend==20201001.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -388,6 +397,9 @@ httplib2==0.10.3
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.12
+# homeassistant.components.hyperion
+hyperion-py==0.3.0
+
# homeassistant.components.iaqualink
iaqualink==0.3.4
@@ -477,6 +489,9 @@ numpy==1.19.2
# homeassistant.components.google
oauth2client==4.0.0
+# homeassistant.components.omnilogic
+omnilogic==0.4.0
+
# homeassistant.components.onvif
onvif-zeep-async==0.5.0
@@ -488,7 +503,7 @@ ovoenergy==1.1.7
# homeassistant.components.mqtt
# homeassistant.components.shiftr
-paho-mqtt==1.5.0
+paho-mqtt==1.5.1
# homeassistant.components.panasonic_viera
panasonic_viera==0.3.6
@@ -515,13 +530,13 @@ pilight==0.1.1
pillow==7.2.0
# homeassistant.components.plex
-plexapi==4.1.0
+plexapi==4.1.1
# homeassistant.components.plex
plexauth==0.0.5
# homeassistant.components.plex
-plexwebsocket==0.0.11
+plexwebsocket==0.0.12
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -564,7 +579,7 @@ py-canary==0.5.0
py-melissa-climate==2.1.4
# homeassistant.components.nightscout
-py-nightscout==1.2.1
+py-nightscout==1.2.2
# homeassistant.components.seventeentrack
py17track==2.2.2
@@ -580,10 +595,10 @@ pyHS100==0.3.5.1
pyMetno==0.8.1
# homeassistant.components.rfxtrx
-pyRFXtrx==0.25
+pyRFXtrx==0.26
# homeassistant.components.tibber
-pyTibber==0.15.2
+pyTibber==0.15.3
# homeassistant.components.nextbus
py_nextbusnext==0.1.4
@@ -592,7 +607,7 @@ py_nextbusnext==0.1.4
pyaehw4a1==0.3.9
# homeassistant.components.airvisual
-pyairvisual==4.4.0
+pyairvisual==5.0.2
# homeassistant.components.almond
pyalmond==0.0.2
@@ -601,7 +616,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
-pyatag==0.3.3.4
+pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.0.0
@@ -613,10 +628,10 @@ pyblackbird==0.5
pybotvac==0.0.17
# homeassistant.components.cast
-pychromecast==7.2.1
+pychromecast==7.5.0
# homeassistant.components.coolmaster
-pycoolmasternet-async==0.1.1
+pycoolmasternet-async==0.1.2
# homeassistant.components.avri
pycountry==19.8.18
@@ -625,7 +640,7 @@ pycountry==19.8.18
pydaikin==2.3.1
# homeassistant.components.deconz
-pydeconz==72
+pydeconz==73
# homeassistant.components.dexcom
pydexcom==0.2.0
@@ -662,19 +677,19 @@ pygatt[GATTTOOL]==4.0.5
pygti==0.6.0
# homeassistant.components.version
-pyhaversion==3.3.0
+pyhaversion==3.4.2
# homeassistant.components.heos
pyheos==0.6.0
# homeassistant.components.homematic
-pyhomematic==0.1.68
+pyhomematic==0.1.70
# homeassistant.components.icloud
pyicloud==0.9.7
# homeassistant.components.insteon
-pyinsteon==1.0.7
+pyinsteon==1.0.8
# homeassistant.components.ipma
pyipma==2.0.5
@@ -692,7 +707,7 @@ pyisy==2.0.2
pykira==0.1.1
# homeassistant.components.kodi
-pykodi==0.1.2
+pykodi==0.2.1
# homeassistant.components.lastfm
pylast==3.3.0
@@ -704,13 +719,13 @@ pylibrespot-java==0.1.0
pylitejet==0.1
# homeassistant.components.lutron_caseta
-pylutron-caseta==0.6.1
+pylutron-caseta==0.7.0
# homeassistant.components.mailgun
pymailgunner==1.4
# homeassistant.components.firmata
-pymata-express==1.13
+pymata-express==1.19
# homeassistant.components.melcloud
pymelcloud==2.5.2
@@ -734,7 +749,7 @@ pymyq==2.0.5
pynut2==2.1.2
# homeassistant.components.nws
-pynws==1.2.1
+pynws==1.3.0
# homeassistant.components.nx584
pynx584==0.5
@@ -762,6 +777,9 @@ pyowm==2.10.0
# homeassistant.components.point
pypoint==1.1.2
+# homeassistant.components.profiler
+pyprof2calltree==1.4.5
+
# homeassistant.components.ps4
pyps4-2ndscreen==1.1.1
@@ -769,7 +787,7 @@ pyps4-2ndscreen==1.1.1
pyqwikswitch==0.93
# homeassistant.components.risco
-pyrisco==0.3.0
+pyrisco==0.3.1
# homeassistant.components.acer_projector
# homeassistant.components.zha
@@ -788,13 +806,13 @@ pysmappee==0.2.13
pysmartapp==0.3.2
# homeassistant.components.smartthings
-pysmartthings==0.7.3
+pysmartthings==0.7.4
# homeassistant.components.soma
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.33
+pysonos==0.0.34
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -824,7 +842,7 @@ python-miio==0.5.3
python-nest==4.1.0
# homeassistant.components.ozw
-python-openzwave-mqtt==1.0.5
+python-openzwave-mqtt==1.2.0
# homeassistant.components.songpal
python-songpal==0.12
@@ -839,7 +857,7 @@ python-tado==0.8.1
python-twitch-client==0.6.0
# homeassistant.components.velbus
-python-velbus==2.0.44
+python-velbus==2.0.46
# homeassistant.components.awair
python_awair==0.1.1
@@ -854,10 +872,10 @@ pytraccar==0.9.0
pytradfri[async]==7.0.2
# homeassistant.components.vera
-pyvera==0.3.9
+pyvera==0.3.10
# homeassistant.components.vesync
-pyvesync==1.1.0
+pyvesync==1.2.0
# homeassistant.components.vizio
pyvizio==0.1.56
@@ -898,6 +916,9 @@ roombapy==1.6.1
# homeassistant.components.roon
roonapi==0.0.21
+# homeassistant.components.rpi_power
+rpi-bad-power==0.0.3
+
# homeassistant.components.yamaha
rxv==0.6.0
@@ -912,7 +933,7 @@ samsungtvws==1.4.0
sense_energy==0.8.0
# homeassistant.components.sentry
-sentry-sdk==0.17.4
+sentry-sdk==0.18.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -921,7 +942,10 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==9.3.0
+simplisafe-python==9.4.1
+
+# homeassistant.components.slack
+slackclient==2.5.0
# homeassistant.components.sleepiq
sleepyq==0.7
@@ -942,7 +966,7 @@ solaredge==0.0.2
somecomfort==0.5.2
# homeassistant.components.sonarr
-sonarr==0.2.3
+sonarr==0.3.0
# homeassistant.components.marytts
speak2mary==1.4.0
@@ -954,7 +978,7 @@ speedtest-cli==2.1.2
spiderpy==1.3.1
# homeassistant.components.spotify
-spotipy==2.14.0
+spotipy==2.16.0
# homeassistant.components.recorder
# homeassistant.components.sql
@@ -976,7 +1000,7 @@ stringcase==1.2.0
sunwatcher==0.2.1
# homeassistant.components.surepetcare
-surepy==0.2.5
+surepy==0.2.6
# homeassistant.components.tellduslive
tellduslive==0.10.11
@@ -1040,7 +1064,7 @@ withings-api==2.1.6
wled==0.4.4
# homeassistant.components.wolflink
-wolf_smartset==0.1.4
+wolf_smartset==0.1.6
# homeassistant.components.bluesound
# homeassistant.components.rest
@@ -1056,13 +1080,13 @@ yeelight==0.5.3
zeroconf==0.28.5
# homeassistant.components.zha
-zha-quirks==0.0.44
+zha-quirks==0.0.45
# homeassistant.components.zha
zigpy-cc==0.5.2
# homeassistant.components.zha
-zigpy-deconz==0.9.2
+zigpy-deconz==0.10.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
@@ -1071,7 +1095,10 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.6.2
# homeassistant.components.zha
-zigpy-znp==0.1.1
+zigpy-znp==0.2.1
# homeassistant.components.zha
-zigpy==0.23.2
+zigpy==0.25.0
+
+# homeassistant.components.zoneminder
+zm-py==0.4.0
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 06b8430a7403dc..a8b10eb4501d91 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -4,8 +4,8 @@ bandit==1.6.2
black==20.8b1
codespell==1.17.1
flake8-docstrings==1.5.0
-flake8==3.8.3
-isort==5.5.2
+flake8==3.8.4
+isort==5.5.3
pydocstyle==5.1.1
pyupgrade==2.7.2
yamllint==1.24.2
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 62b1a22cc73cd8..c3e489c1ebb8e9 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -69,6 +69,10 @@
# Constrain httplib2 to protect against CVE-2020-11078
httplib2>=0.18.0
+# gRPC 1.32+ currently causes issues on ARMv7, see:
+# https://github.com/home-assistant/core/issues/40148
+grpcio==1.31.0
+
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py
index a4f918684dc4aa..e1312148cbe1b2 100644
--- a/script/scaffold/templates/device_trigger/integration/device_trigger.py
+++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py
@@ -3,8 +3,9 @@
import voluptuous as vol
-from homeassistant.components.automation import AutomationActionType, state
+from homeassistant.components.automation import AutomationActionType
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.homeassistant.triggers import state
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DOMAIN,
diff --git a/setup.cfg b/setup.cfg
index 8dd3e083a8b99a..bf863cb97a5821 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,10 +14,6 @@ classifier =
Programming Language :: Python :: 3.7
Topic :: Home Automation
-[tool:pytest]
-testpaths = tests
-norecursedirs = .git testing_config
-
[flake8]
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
doctests = True
@@ -35,16 +31,6 @@ ignore =
D202,
W504
-[isort]
-# https://github.com/timothycrosley/isort
-# https://github.com/timothycrosley/isort/wiki/isort-Settings
-profile = black
-# will group `import x` and `from x import` of the same module.
-force_sort_within_sections = true
-known_first_party = homeassistant,tests
-forced_separate = tests
-combine_as_imports = true
-
[mypy]
python_version = 3.7
ignore_errors = true
diff --git a/setup.py b/setup.py
index 0bbdf9f05a8fc0..022f5547655d4d 100755
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,7 @@
"pyyaml==5.3.1",
"requests==2.24.0",
"ruamel.yaml==0.15.100",
- "voluptuous==0.11.7",
+ "voluptuous==0.12.0",
"voluptuous-serialize==2.4.0",
"yarl==1.4.2",
]
diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py
index f9ac8fbb92a30b..c804b237e8b7f4 100644
--- a/tests/auth/providers/test_homeassistant.py
+++ b/tests/auth/providers/test_homeassistant.py
@@ -277,7 +277,7 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data):
async def test_race_condition_in_data_loading(hass):
"""Test race condition in the hass_auth.Data loading.
- Ref issue: https://github.com/home-assistant/home-assistant/issues/21569
+ Ref issue: https://github.com/home-assistant/core/issues/21569
"""
counter = 0
diff --git a/tests/common.py b/tests/common.py
index 1cba478767fcff..d516873786bac3 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -205,6 +205,7 @@ def async_create_task(coroutine):
hass.config.elevation = 0
hass.config.time_zone = date_util.get_time_zone("US/Pacific")
hass.config.units = METRIC_SYSTEM
+ hass.config.media_dirs = {"local": get_test_config_dir("media")}
hass.config.skip_pip = True
hass.config_entries = config_entries.ConfigEntries(hass, {})
diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py
index 01508d412a2754..509a68eda4bf15 100644
--- a/tests/components/abode/test_config_flow.py
+++ b/tests/components/abode/test_config_flow.py
@@ -61,7 +61,7 @@ async def test_invalid_credentials(hass):
side_effect=AbodeAuthenticationException((400, "auth error")),
):
result = await flow.async_step_user(user_input=conf)
- assert result["errors"] == {"base": "invalid_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
async def test_connection_error(hass):
@@ -78,7 +78,7 @@ async def test_connection_error(hass):
),
):
result = await flow.async_step_user(user_input=conf)
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_step_import(hass):
diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py
index b94d17066c8d77..4ce34dfebe99ef 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -6,7 +6,6 @@
ATTRIBUTION,
CONCENTRATION_PARTS_PER_CUBIC_METER,
DOMAIN,
- LENGTH_MILIMETERS,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
@@ -17,6 +16,7 @@
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_TEMPERATURE,
LENGTH_METERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_KILOMETERS_PER_HOUR,
STATE_UNAVAILABLE,
@@ -52,7 +52,7 @@ async def test_sensor_without_forecast(hass):
assert state
assert state.state == "0.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
- assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILIMETERS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS
assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy"
assert state.attributes.get("type") is None
diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py
index 98a23a0dd4131a..2aacedc36801e0 100644
--- a/tests/components/acmeda/test_config_flow.py
+++ b/tests/components/acmeda/test_config_flow.py
@@ -47,7 +47,7 @@ async def test_show_form_no_hubs(hass, mock_hub_discover):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "all_configured"
+ assert result["reason"] == "no_devices_found"
# Check we performed the discovery
assert len(mock_hub_discover.mock_calls) == 1
@@ -140,4 +140,4 @@ async def test_already_configured(hass, mock_hub_discover):
)
assert result["type"] == "abort"
- assert result["reason"] == "all_configured"
+ assert result["reason"] == "no_devices_found"
diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
index dae9e0da79dd3c..36263335dacca7 100644
--- a/tests/components/adguard/test_config_flow.py
+++ b/tests/components/adguard/test_config_flow.py
@@ -12,6 +12,7 @@
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
from tests.async_mock import patch
@@ -52,7 +53,7 @@ async def test_connection_error(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_full_flow_implementation(hass, aioclient_mock):
@@ -62,7 +63,7 @@ async def test_full_flow_implementation(hass, aioclient_mock):
f"://{FIXTURE_USER_INPUT[CONF_HOST]}"
f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status",
json={"version": "v0.99.0"},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.AdGuardHomeFlowHandler()
@@ -134,12 +135,12 @@ async def test_hassio_update_instance_running(hass, aioclient_mock):
aioclient_mock.get(
"http://mock-adguard-updated:3000/control/status",
json={"version": "v0.99.0"},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://mock-adguard:3000/control/status",
json={"version": "v0.99.0"},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
@@ -195,7 +196,7 @@ async def test_hassio_confirm(hass, aioclient_mock):
aioclient_mock.get(
"http://mock-adguard:3000/control/status",
json={"version": "v0.99.0"},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -234,4 +235,4 @@ async def test_hassio_connection_error(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "hassio_confirm"
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py
index 2cda0b8aa73b62..ec35b521a1721e 100644
--- a/tests/components/agent_dvr/__init__.py
+++ b/tests/components/agent_dvr/__init__.py
@@ -1,7 +1,7 @@
"""Tests for the agent_dvr component."""
from homeassistant.components.agent_dvr.const import DOMAIN, SERVER_URL
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@@ -18,12 +18,12 @@ async def init_integration(
aioclient_mock.get(
"http://example.local:8090/command.cgi?cmd=getStatus",
text=load_fixture("agent_dvr/status.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://example.local:8090/command.cgi?cmd=getObjects",
text=load_fixture("agent_dvr/objects.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
domain=DOMAIN,
diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py
index 403dd9f4bee853..064034a4a69a91 100644
--- a/tests/components/agent_dvr/test_config_flow.py
+++ b/tests/components/agent_dvr/test_config_flow.py
@@ -3,7 +3,7 @@
from homeassistant.components.agent_dvr import config_flow
from homeassistant.components.agent_dvr.const import SERVER_URL
from homeassistant.config_entries import SOURCE_USER
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -61,13 +61,13 @@ async def test_full_user_flow_implementation(
aioclient_mock.get(
"http://example.local:8090/command.cgi?cmd=getStatus",
text=load_fixture("agent_dvr/status.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://example.local:8090/command.cgi?cmd=getObjects",
text=load_fixture("agent_dvr/objects.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py
index 243a92258eb00f..d7d45bbd7e3fec 100644
--- a/tests/components/airly/test_config_flow.py
+++ b/tests/components/airly/test_config_flow.py
@@ -48,7 +48,7 @@ async def test_invalid_api_key(hass):
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
- assert result["errors"] == {"base": "auth"}
+ assert result["errors"] == {"base": "invalid_api_key"}
async def test_invalid_location(hass):
diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py
index d365720ad262ea..bcfd34cd58960e 100644
--- a/tests/components/airvisual/test_config_flow.py
+++ b/tests/components/airvisual/test_config_flow.py
@@ -69,7 +69,7 @@ async def test_invalid_identifier(hass):
}
with patch(
- "pyairvisual.api.API.nearest_city",
+ "pyairvisual.air_quality.AirQuality",
side_effect=InvalidKeyError,
):
result = await hass.config_entries.flow.async_init(
@@ -96,7 +96,7 @@ async def test_migration(hass):
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
- with patch("pyairvisual.api.API.nearest_city"), patch.object(
+ with patch("pyairvisual.air_quality.AirQuality.nearest_city"), patch.object(
hass.config_entries, "async_forward_entry_setup"
):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf})
@@ -130,7 +130,7 @@ async def test_node_pro_error(hass):
node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
with patch(
- "pyairvisual.node.Node.from_samba",
+ "pyairvisual.node.NodeSamba.async_connect",
side_effect=NodeProError,
):
result = await hass.config_entries.flow.async_init(
@@ -140,7 +140,7 @@ async def test_node_pro_error(hass):
result["flow_id"], user_input=node_pro_conf
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_IP_ADDRESS: "unable_to_connect"}
+ assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
async def test_options_flow(hass):
@@ -185,7 +185,7 @@ async def test_step_geography(hass):
with patch(
"homeassistant.components.airvisual.async_setup_entry", return_value=True
- ), patch("pyairvisual.api.API.nearest_city"):
+ ), patch("pyairvisual.air_quality.AirQuality.nearest_city"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
@@ -209,7 +209,7 @@ async def test_step_import(hass):
with patch(
"homeassistant.components.airvisual.async_setup_entry", return_value=True
- ), patch("pyairvisual.api.API.nearest_city"):
+ ), patch("pyairvisual.air_quality.AirQuality.nearest_city"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
)
@@ -230,7 +230,11 @@ async def test_step_node_pro(hass):
with patch(
"homeassistant.components.airvisual.async_setup_entry", return_value=True
- ), patch("pyairvisual.node.Node.from_samba"):
+ ), patch("pyairvisual.node.NodeSamba.async_connect"), patch(
+ "pyairvisual.node.NodeSamba.async_get_latest_measurements"
+ ), patch(
+ "pyairvisual.node.NodeSamba.async_disconnect"
+ ):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"}
)
@@ -268,8 +272,8 @@ async def test_step_reauth(hass):
assert result["step_id"] == "reauth_confirm"
with patch(
- "homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch("pyairvisual.api.API.nearest_city"):
+ "homeassistant.components.airvisual.async_setup_entry", return_value=True
+ ), patch("pyairvisual.air_quality.AirQuality"):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_API_KEY: "defgh67890"}
)
diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py
new file mode 100644
index 00000000000000..64f4a604ff392d
--- /dev/null
+++ b/tests/components/alarmdecoder/test_config_flow.py
@@ -0,0 +1,430 @@
+"""Test the AlarmDecoder config flow."""
+from alarmdecoder.util import NoDeviceError
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.alarmdecoder import config_flow
+from homeassistant.components.alarmdecoder.const import (
+ CONF_ALT_NIGHT_MODE,
+ CONF_AUTO_BYPASS,
+ CONF_CODE_ARM_REQUIRED,
+ CONF_DEVICE_BAUD,
+ CONF_DEVICE_PATH,
+ CONF_RELAY_ADDR,
+ CONF_RELAY_CHAN,
+ CONF_ZONE_LOOP,
+ CONF_ZONE_NAME,
+ CONF_ZONE_NUMBER,
+ CONF_ZONE_RFID,
+ CONF_ZONE_TYPE,
+ DEFAULT_ARM_OPTIONS,
+ DEFAULT_ZONE_OPTIONS,
+ DOMAIN,
+ OPTIONS_ARM,
+ OPTIONS_ZONES,
+ PROTOCOL_SERIAL,
+ PROTOCOL_SOCKET,
+)
+from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
+from homeassistant.core import HomeAssistant
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+@pytest.mark.parametrize(
+ "protocol,connection,title",
+ [
+ (
+ PROTOCOL_SOCKET,
+ {
+ CONF_HOST: "alarmdecoder123",
+ CONF_PORT: 10001,
+ },
+ "alarmdecoder123:10001",
+ ),
+ (
+ PROTOCOL_SERIAL,
+ {
+ CONF_DEVICE_PATH: "/dev/ttyUSB123",
+ CONF_DEVICE_BAUD: 115000,
+ },
+ "/dev/ttyUSB123",
+ ),
+ ],
+)
+async def test_setups(hass: HomeAssistant, protocol, connection, title):
+ """Test flow for setting up the available AlarmDecoder protocols."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.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"],
+ {CONF_PROTOCOL: protocol},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "protocol"
+
+ with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch(
+ "homeassistant.components.alarmdecoder.config_flow.AdExt.close"
+ ), patch(
+ "homeassistant.components.alarmdecoder.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.alarmdecoder.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], connection
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == title
+ assert result["data"] == {
+ **connection,
+ CONF_PROTOCOL: protocol,
+ }
+
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_setup_connection_error(hass: HomeAssistant):
+ """Test flow for setup with a connection error."""
+
+ port = 1001
+ host = "alarmdecoder"
+ protocol = PROTOCOL_SOCKET
+ connection_settings = {CONF_HOST: host, CONF_PORT: port}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.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"],
+ {CONF_PROTOCOL: protocol},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "protocol"
+
+ with patch(
+ "homeassistant.components.alarmdecoder.config_flow.AdExt.open",
+ side_effect=NoDeviceError,
+ ), patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], connection_settings
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "service_unavailable"}
+
+
+async def test_options_arm_flow(hass: HomeAssistant):
+ """Test arm options flow."""
+ user_input = {
+ CONF_ALT_NIGHT_MODE: True,
+ CONF_AUTO_BYPASS: True,
+ CONF_CODE_ARM_REQUIRED: True,
+ }
+ entry = MockConfigEntry(domain=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)
+
+ 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={"edit_selection": "Arming Settings"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "arm_settings"
+
+ with patch(
+ "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input=user_input,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert entry.options == {
+ OPTIONS_ARM: user_input,
+ OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS,
+ }
+
+
+async def test_options_zone_flow(hass: HomeAssistant):
+ """Test options flow for adding/deleting zones."""
+ zone_number = "2"
+ zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW}
+ entry = MockConfigEntry(domain=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)
+
+ 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={"edit_selection": "Zones"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_select"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_ZONE_NUMBER: zone_number},
+ )
+
+ with patch(
+ "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input=zone_settings,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert entry.options == {
+ OPTIONS_ARM: DEFAULT_ARM_OPTIONS,
+ OPTIONS_ZONES: {zone_number: zone_settings},
+ }
+
+ # Make sure zone can be removed...
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ 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={"edit_selection": "Zones"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_select"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_ZONE_NUMBER: zone_number},
+ )
+
+ with patch(
+ "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert entry.options == {
+ OPTIONS_ARM: DEFAULT_ARM_OPTIONS,
+ OPTIONS_ZONES: {},
+ }
+
+
+async def test_options_zone_flow_validation(hass: HomeAssistant):
+ """Test input validation for zone options flow."""
+ zone_number = "2"
+ zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW}
+ entry = MockConfigEntry(domain=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)
+
+ 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={"edit_selection": "Zones"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_select"
+
+ # Zone Number must be int
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_ZONE_NUMBER: "asd"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_select"
+ assert result["errors"] == {CONF_ZONE_NUMBER: "int"}
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_ZONE_NUMBER: zone_number},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+
+ # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_RELAY_ADDR: "1"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {"base": "relay_inclusive"}
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_RELAY_CHAN: "1"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {"base": "relay_inclusive"}
+
+ # CONF_RELAY_ADDR, CONF_RELAY_CHAN must be int
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {
+ CONF_RELAY_ADDR: "int",
+ CONF_RELAY_CHAN: "int",
+ }
+
+ # CONF_ZONE_LOOP depends on CONF_ZONE_RFID
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_ZONE_LOOP: "1"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"}
+
+ # CONF_ZONE_LOOP must be int
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {CONF_ZONE_LOOP: "int"}
+
+ # CONF_ZONE_LOOP must be between [1,4]
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "zone_details"
+ assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"}
+
+ # All valid settings
+ with patch(
+ "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ **zone_settings,
+ CONF_ZONE_RFID: "rfid123",
+ CONF_ZONE_LOOP: "2",
+ CONF_RELAY_ADDR: "12",
+ CONF_RELAY_CHAN: "1",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert entry.options == {
+ OPTIONS_ARM: DEFAULT_ARM_OPTIONS,
+ OPTIONS_ZONES: {
+ zone_number: {
+ **zone_settings,
+ CONF_ZONE_RFID: "rfid123",
+ CONF_ZONE_LOOP: 2,
+ CONF_RELAY_ADDR: 12,
+ CONF_RELAY_CHAN: 1,
+ }
+ },
+ }
+
+
+@pytest.mark.parametrize(
+ "protocol,connection",
+ [
+ (
+ PROTOCOL_SOCKET,
+ {
+ CONF_HOST: "alarmdecoder123",
+ CONF_PORT: 10001,
+ },
+ ),
+ (
+ PROTOCOL_SERIAL,
+ {
+ CONF_DEVICE_PATH: "/dev/ttyUSB123",
+ CONF_DEVICE_BAUD: 115000,
+ },
+ ),
+ ],
+)
+async def test_one_device_allowed(hass, protocol, connection):
+ """Test that only one AlarmDecoder device is allowed."""
+ flow = config_flow.AlarmDecoderFlowHandler()
+ flow.hass = hass
+
+ MockConfigEntry(
+ domain=DOMAIN,
+ data=connection,
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.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"],
+ {CONF_PROTOCOL: protocol},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "protocol"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], connection
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py
index 8937a7938ac80f..c838bf5b3a3d63 100644
--- a/tests/components/alexa/test_intent.py
+++ b/tests/components/alexa/test_intent.py
@@ -6,6 +6,7 @@
from homeassistant.components import alexa
from homeassistant.components.alexa import intent
+from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
@@ -111,7 +112,7 @@ def _intent_req(client, data=None):
return client.post(
intent.INTENTS_API_ENDPOINT,
data=json.dumps(data or {}),
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py
index 46db47dc2c9481..c46a61aef41278 100644
--- a/tests/components/alexa/test_smart_home_http.py
+++ b/tests/components/alexa/test_smart_home_http.py
@@ -2,7 +2,7 @@
import json
from homeassistant.components.alexa import DOMAIN, smart_home_http
-from homeassistant.const import HTTP_NOT_FOUND
+from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND
from homeassistant.setup import async_setup_component
from . import get_new_request
@@ -17,7 +17,7 @@ async def do_http_discovery(config, hass, hass_client):
response = await http_client.post(
smart_home_http.SMART_HOME_HTTP_ENDPOINT,
data=json.dumps(request),
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
return response
diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py
index 49694d844104cc..b2144205895430 100644
--- a/tests/components/almond/test_config_flow.py
+++ b/tests/components/almond/test_config_flow.py
@@ -80,15 +80,15 @@ async def test_abort_if_existing_entry(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
result = await flow.async_step_import()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
result = await flow.async_step_hassio({})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py
index 35c2ef69bb39c6..f7723c73d53c0c 100644
--- a/tests/components/ambiclimate/test_config_flow.py
+++ b/tests/components/ambiclimate/test_config_flow.py
@@ -30,7 +30,7 @@ async def test_abort_if_no_implementation_registered(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "no_config"
+ assert result["reason"] == "oauth2_missing_configuration"
async def test_abort_if_already_setup(hass):
@@ -40,12 +40,12 @@ async def test_abort_if_already_setup(hass):
with patch.object(hass.config_entries, "async_entries", return_value=[{}]):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "already_configured_account"
with patch.object(hass.config_entries, "async_entries", return_value=[{}]):
result = await flow.async_step_code()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "already_configured_account"
async def test_full_flow_implementation(hass):
@@ -107,7 +107,7 @@ async def test_already_setup(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "already_configured_account"
async def test_view(hass):
diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py
index 456a9313091d4e..c497a9daa485d3 100644
--- a/tests/components/androidtv/test_media_player.py
+++ b/tests/components/androidtv/test_media_player.py
@@ -1064,6 +1064,21 @@ async def test_get_image(hass, hass_ws_client):
assert msg["result"]["content_type"] == "image/png"
assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8")
+ with patch(
+ "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap",
+ side_effect=RuntimeError,
+ ):
+ await client.send_json(
+ {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id}
+ )
+
+ msg = await client.receive_json()
+
+ # The device is unavailable, but getting the media image did not cause an exception
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == STATE_UNAVAILABLE
+
async def _test_service(
hass,
diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py
index 032f57c3113547..7de4b83723e17a 100644
--- a/tests/components/arcam_fmj/test_config_flow.py
+++ b/tests/components/arcam_fmj/test_config_flow.py
@@ -99,7 +99,7 @@ async def test_ssdp_unable_to_connect(hass, dummy_client):
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "unable_to_connect"
+ assert result["reason"] == "cannot_connect"
async def test_ssdp_update(hass):
diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py
index 3f2b6468491fd1..52d53ee99480cb 100644
--- a/tests/components/atag/__init__.py
+++ b/tests/components/atag/__init__.py
@@ -1,7 +1,7 @@
"""Tests for the Atag integration."""
from homeassistant.components.atag import DOMAIN
-from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -59,20 +59,20 @@ async def init_integration(
) -> MockConfigEntry:
"""Set up the Atag integration in Home Assistant."""
- aioclient_mock.get(
+ aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
json=RECEIVE_REPLY,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/update",
json=UPDATE_REPLY,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT)
diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py
index 63cac2a0e9cec9..7609d6c3e545d1 100644
--- a/tests/components/atag/test_config_flow.py
+++ b/tests/components/atag/test_config_flow.py
@@ -78,14 +78,14 @@ async def test_full_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test registering an integration and finishing flow works."""
- aioclient_mock.get(
- "http://127.0.0.1:10000/retrieve",
- json=RECEIVE_REPLY,
- )
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
)
+ aioclient_mock.post(
+ "http://127.0.0.1:10000/retrieve",
+ json=RECEIVE_REPLY,
+ )
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py
index 0fca4b37c46014..9f7ae9cb4edecb 100644
--- a/tests/components/atag/test_init.py
+++ b/tests/components/atag/test_init.py
@@ -14,7 +14,7 @@ async def test_config_entry_not_ready(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test configuration entry not ready on library error."""
- aioclient_mock.get("http://127.0.0.1:10000/retrieve", exc=aiohttp.ClientError)
+ aioclient_mock.post("http://127.0.0.1:10000/retrieve", exc=aiohttp.ClientError)
entry = await init_integration(hass, aioclient_mock)
assert entry.state == ENTRY_STATE_SETUP_RETRY
diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py
index c471dfca2a9dda..93b64ebbd3fd13 100644
--- a/tests/components/august/mocks.py
+++ b/tests/components/august/mocks.py
@@ -43,12 +43,21 @@ def _mock_get_config():
}
+def _mock_authenticator(auth_state):
+ """Mock an august authenticator."""
+ authenticator = MagicMock()
+ type(authenticator).state = PropertyMock(return_value=auth_state)
+ return authenticator
+
+
@patch("homeassistant.components.august.gateway.ApiAsync")
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
"""Set up august integration."""
authenticate_mock.side_effect = MagicMock(
- return_value=_mock_august_authentication("original_token", 1234)
+ return_value=_mock_august_authentication(
+ "original_token", 1234, AuthenticationState.AUTHENTICATED
+ )
)
api_mock.return_value = api_instance
assert await async_setup_component(hass, DOMAIN, _mock_get_config())
@@ -185,11 +194,9 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
return await _mock_setup_august(hass, api_instance)
-def _mock_august_authentication(token_text, token_timestamp):
+def _mock_august_authentication(token_text, token_timestamp, state):
authentication = MagicMock(name="august.authentication")
- type(authentication).state = PropertyMock(
- return_value=AuthenticationState.AUTHENTICATED
- )
+ type(authentication).state = PropertyMock(return_value=state)
type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock(
return_value=token_timestamp
diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py
index 2f20347acaeeeb..1c23976a6f9769 100644
--- a/tests/components/august/test_config_flow.py
+++ b/tests/components/august/test_config_flow.py
@@ -17,6 +17,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from tests.async_mock import patch
+from tests.common import MockConfigEntry
async def test_form(hass):
@@ -84,6 +85,29 @@ async def test_form_invalid_auth(hass):
assert result2["errors"] == {"base": "invalid_auth"}
+async def test_user_unexpected_exception(hass):
+ """Test we handle an unexpected exception."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
+ side_effect=ValueError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_LOGIN_METHOD: "email",
+ CONF_USERNAME: "my@email.tld",
+ CONF_PASSWORD: "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
@@ -197,3 +221,49 @@ async def test_form_needs_validate(hass):
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_reauth(hass):
+ """Test reauthenticate."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_LOGIN_METHOD: "email",
+ CONF_USERNAME: "my@email.tld",
+ CONF_PASSWORD: "test-password",
+ CONF_INSTALL_ID: None,
+ CONF_TIMEOUT: 10,
+ CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
+ },
+ unique_id="my@email.tld",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.august.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.august.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_PASSWORD: "new-test-password",
+ },
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "reauth_successful"
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py
index ec035b9ec38bac..c1aa0723baac2e 100644
--- a/tests/components/august/test_gateway.py
+++ b/tests/components/august/test_gateway.py
@@ -1,4 +1,6 @@
"""The gateway tests for the august platform."""
+from august.authenticator_common import AuthenticationState
+
from homeassistant.components.august.const import DOMAIN
from homeassistant.components.august.gateway import AugustGateway
@@ -11,6 +13,7 @@ async def test_refresh_access_token(hass):
await _patched_refresh_access_token(hass, "new_token", 5678)
+@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks")
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh")
@patch(
@@ -23,9 +26,12 @@ async def _patched_refresh_access_token(
refresh_access_token_mock,
should_refresh_mock,
authenticate_mock,
+ async_get_operable_locks_mock,
):
authenticate_mock.side_effect = MagicMock(
- return_value=_mock_august_authentication("original_token", 1234)
+ return_value=_mock_august_authentication(
+ "original_token", 1234, AuthenticationState.AUTHENTICATED
+ )
)
august_gateway = AugustGateway(hass)
mocked_config = _mock_get_config()
@@ -38,7 +44,7 @@ async def _patched_refresh_access_token(
should_refresh_mock.return_value = True
refresh_access_token_mock.return_value = _mock_august_authentication(
- new_token, new_token_expire_time
+ new_token, new_token_expire_time, AuthenticationState.AUTHENTICATED
)
await august_gateway.async_refresh_access_token_if_needed()
refresh_access_token_mock.assert_called()
diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py
index bcc05e51c71283..f954ff83c25a46 100644
--- a/tests/components/august/test_init.py
+++ b/tests/components/august/test_init.py
@@ -1,6 +1,8 @@
"""The tests for the august platform."""
import asyncio
+from aiohttp import ClientResponseError
+from august.authenticator_common import AuthenticationState
from august.exceptions import AugustApiAIOHTTPError
from homeassistant import setup
@@ -12,7 +14,10 @@
DOMAIN,
)
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
-from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
+from homeassistant.config_entries import (
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PASSWORD,
@@ -30,6 +35,7 @@
from tests.common import MockConfigEntry
from tests.components.august.mocks import (
_create_august_with_devices,
+ _mock_august_authentication,
_mock_doorsense_enabled_august_lock_detail,
_mock_doorsense_missing_august_lock_detail,
_mock_get_config,
@@ -54,8 +60,8 @@ async def test_august_is_offline(hass):
side_effect=asyncio.TimeoutError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
- await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
@@ -158,7 +164,7 @@ async def test_set_up_from_yaml(hass):
await hass.async_block_till_done()
assert len(mock_setup_august.mock_calls) == 1
call = mock_setup_august.call_args
- args, kwargs = call
+ args, _ = call
imported_config_entry = args[1]
# The import must use DEFAULT_AUGUST_CONFIG_FILE so they
# do not loose their token when config is migrated
@@ -170,3 +176,133 @@ async def test_set_up_from_yaml(hass):
CONF_TIMEOUT: None,
CONF_USERNAME: "mocked_username",
}
+
+
+async def test_auth_fails(hass):
+ """Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ title="August august",
+ )
+ config_entry.add_to_hass(hass)
+ assert hass.config_entries.flow.async_progress() == []
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ side_effect=ClientResponseError(None, None, status=401),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+
+ assert flows[0]["step_id"] == "user"
+
+
+async def test_bad_password(hass):
+ """Config entry state is ENTRY_STATE_SETUP_ERROR when the password has been changed."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ title="August august",
+ )
+ config_entry.add_to_hass(hass)
+ assert hass.config_entries.flow.async_progress() == []
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ return_value=_mock_august_authentication(
+ "original_token", 1234, AuthenticationState.BAD_PASSWORD
+ ),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+
+ assert flows[0]["step_id"] == "user"
+
+
+async def test_http_failure(hass):
+ """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ title="August august",
+ )
+ config_entry.add_to_hass(hass)
+ assert hass.config_entries.flow.async_progress() == []
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ side_effect=ClientResponseError(None, None, status=500),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_RETRY
+
+ assert hass.config_entries.flow.async_progress() == []
+
+
+async def test_unknown_auth_state(hass):
+ """Config entry state is ENTRY_STATE_SETUP_ERROR when august is in an unknown auth state."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ title="August august",
+ )
+ config_entry.add_to_hass(hass)
+ assert hass.config_entries.flow.async_progress() == []
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ return_value=_mock_august_authentication("original_token", 1234, None),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+
+ assert flows[0]["step_id"] == "user"
+
+
+async def test_requires_validation_state(hass):
+ """Config entry state is ENTRY_STATE_SETUP_ERROR when august requires validation."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ title="August august",
+ )
+ config_entry.add_to_hass(hass)
+ assert hass.config_entries.flow.async_progress() == []
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ return_value=_mock_august_authentication(
+ "original_token", 1234, AuthenticationState.REQUIRES_VALIDATION
+ ),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+ assert hass.config_entries.flow.async_progress() == []
diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py
index 7e69b59da07058..51e00b9d09fb9b 100644
--- a/tests/components/august/test_sensor.py
+++ b/tests/components/august/test_sensor.py
@@ -1,6 +1,6 @@
"""The sensor tests for the august platform."""
-from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE
from tests.components.august.mocks import (
_create_august_with_devices,
@@ -75,7 +75,7 @@ async def test_create_lock_with_linked_keypad(hass):
state = hass.states.get("sensor.front_door_lock_keypad_battery")
assert state.state == "60"
- assert state.attributes["unit_of_measurement"] == PERCENTAGE
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery")
assert entry
assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery"
@@ -105,7 +105,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass):
state = hass.states.get("sensor.front_door_lock_keypad_battery")
assert state.state == "10"
- assert state.attributes["unit_of_measurement"] == PERCENTAGE
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery")
assert entry
assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery"
diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py
index ea385d5697fdb1..d4eea423244617 100644
--- a/tests/components/aurora/test_binary_sensor.py
+++ b/tests/components/aurora/test_binary_sensor.py
@@ -1,85 +1,60 @@
"""The tests for the Aurora sensor platform."""
import re
-import unittest
-
-import requests_mock
from homeassistant.components.aurora import binary_sensor as aurora
-from tests.common import get_test_home_assistant, load_fixture
+from tests.common import load_fixture
-class TestAuroraSensorSetUp(unittest.TestCase):
- """Test the aurora platform."""
+def test_setup_and_initial_state(hass, requests_mock):
+ """Test that the component is created and initialized as expected."""
+ uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt")
+ requests_mock.get(uri, text=load_fixture("aurora.txt"))
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.lat = 37.8267
- self.lon = -122.423
- self.hass.config.latitude = self.lat
- self.hass.config.longitude = self.lon
- self.entities = []
- self.addCleanup(self.tear_down_cleanup)
+ entities = []
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
+ def mock_add_entities(new_entities, update_before_add=False):
+ """Mock add entities."""
+ if update_before_add:
+ for entity in new_entities:
+ entity.update()
- @requests_mock.Mocker()
- def test_setup_and_initial_state(self, mock_req):
- """Test that the component is created and initialized as expected."""
- uri = re.compile(
- r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
- )
- mock_req.get(uri, text=load_fixture("aurora.txt"))
+ for entity in new_entities:
+ entities.append(entity)
- entities = []
+ config = {"name": "Test", "forecast_threshold": 75}
+ aurora.setup_platform(hass, config, mock_add_entities)
- def mock_add_entities(new_entities, update_before_add=False):
- """Mock add entities."""
- if update_before_add:
- for entity in new_entities:
- entity.update()
+ aurora_component = entities[0]
+ assert len(entities) == 1
+ assert aurora_component.name == "Test"
+ assert aurora_component.device_state_attributes["visibility_level"] == "0"
+ assert aurora_component.device_state_attributes["message"] == "nothing's out"
+ assert not aurora_component.is_on
- for entity in new_entities:
- entities.append(entity)
-
- config = {"name": "Test", "forecast_threshold": 75}
- aurora.setup_platform(self.hass, config, mock_add_entities)
-
- aurora_component = entities[0]
- assert len(entities) == 1
- assert aurora_component.name == "Test"
- assert aurora_component.device_state_attributes["visibility_level"] == "0"
- assert aurora_component.device_state_attributes["message"] == "nothing's out"
- assert not aurora_component.is_on
-
- @requests_mock.Mocker()
- def test_custom_threshold_works(self, mock_req):
- """Test that the config can take a custom forecast threshold."""
- uri = re.compile(
- r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
- )
- mock_req.get(uri, text=load_fixture("aurora.txt"))
-
- entities = []
-
- def mock_add_entities(new_entities, update_before_add=False):
- """Mock add entities."""
- if update_before_add:
- for entity in new_entities:
- entity.update()
+def test_custom_threshold_works(hass, requests_mock):
+ """Test that the config can take a custom forecast threshold."""
+ uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt")
+ requests_mock.get(uri, text=load_fixture("aurora.txt"))
+
+ entities = []
+
+ def mock_add_entities(new_entities, update_before_add=False):
+ """Mock add entities."""
+ if update_before_add:
for entity in new_entities:
- entities.append(entity)
+ entity.update()
+
+ for entity in new_entities:
+ entities.append(entity)
- config = {"name": "Test", "forecast_threshold": 1}
- self.hass.config.longitude = 18.987
- self.hass.config.latitude = 69.648
+ config = {"name": "Test", "forecast_threshold": 1}
+ hass.config.longitude = 18.987
+ hass.config.latitude = 69.648
- aurora.setup_platform(self.hass, config, mock_add_entities)
+ aurora.setup_platform(hass, config, mock_add_entities)
- aurora_component = entities[0]
- assert aurora_component.aurora_data.visibility_level == "16"
- assert aurora_component.is_on
+ aurora_component = entities[0]
+ assert aurora_component.aurora_data.visibility_level == "16"
+ assert aurora_component.is_on
diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py
deleted file mode 100644
index 26521f76d31747..00000000000000
--- a/tests/components/automation/common.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Collection of helper methods.
-
-All containing methods are legacy helpers that should not be used by new
-components. Instead call the service directly.
-"""
-from homeassistant.components.automation import (
- CONF_SKIP_CONDITION,
- DOMAIN,
- SERVICE_TRIGGER,
-)
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- ENTITY_MATCH_ALL,
- SERVICE_RELOAD,
- SERVICE_TOGGLE,
- SERVICE_TURN_OFF,
- SERVICE_TURN_ON,
-)
-from homeassistant.loader import bind_hass
-
-
-@bind_hass
-async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL):
- """Turn on specified automation or all."""
- data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)
-
-
-@bind_hass
-async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL):
- """Turn off specified automation or all."""
- data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)
-
-
-@bind_hass
-async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL):
- """Toggle specified automation or all."""
- data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)
-
-
-@bind_hass
-async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL, skip_condition=True):
- """Trigger specified automation or all."""
- data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- data[CONF_SKIP_CONDITION] = skip_condition
- await hass.services.async_call(DOMAIN, SERVICE_TRIGGER, data)
-
-
-@bind_hass
-async def async_reload(hass, context=None):
- """Reload the automation from config."""
- await hass.services.async_call(
- DOMAIN, SERVICE_RELOAD, blocking=True, context=context
- )
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 9c38574945d047..1cdcfc11dfb11d 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -10,12 +10,16 @@
DOMAIN,
EVENT_AUTOMATION_RELOADED,
EVENT_AUTOMATION_TRIGGERED,
+ SERVICE_TRIGGER,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
EVENT_HOMEASSISTANT_STARTED,
+ SERVICE_RELOAD,
+ SERVICE_TOGGLE,
SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
@@ -26,7 +30,6 @@
from tests.async_mock import Mock, patch
from tests.common import assert_setup_component, async_mock_service, mock_restore_cache
-from tests.components.automation import common
from tests.components.logbook.test_init import MockLazyEventPartialState
@@ -402,45 +405,59 @@ async def test_services(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
- await common.async_turn_off(hass, entity_id)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {
+ ATTR_ENTITY_ID: entity_id,
+ },
+ blocking=True,
+ )
assert not automation.is_on(hass, entity_id)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
- await common.async_toggle(hass, entity_id)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert automation.is_on(hass, entity_id)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 2
- await common.async_toggle(hass, entity_id)
- await hass.async_block_till_done()
-
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ )
assert not automation.is_on(hass, entity_id)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 2
- await common.async_toggle(hass, entity_id)
- await hass.async_block_till_done()
-
- await common.async_trigger(hass, entity_id)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TRIGGER, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert len(calls) == 3
- await common.async_turn_off(hass, entity_id)
- await hass.async_block_till_done()
- await common.async_trigger(hass, entity_id)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TRIGGER, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert len(calls) == 4
- await common.async_turn_on(hass, entity_id)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert automation.is_on(hass, entity_id)
@@ -492,10 +509,18 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl
},
):
with pytest.raises(Unauthorized):
- await common.async_reload(hass, Context(user_id=hass_read_only_user.id))
- await hass.async_block_till_done()
- await common.async_reload(hass, Context(user_id=hass_admin_user.id))
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_RELOAD,
+ context=Context(user_id=hass_read_only_user.id),
+ blocking=True,
+ )
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_RELOAD,
+ context=Context(user_id=hass_admin_user.id),
+ blocking=True,
+ )
# De-flake ?!
await hass.async_block_till_done()
@@ -547,8 +572,7 @@ async def test_reload_config_when_invalid_config(hass, calls):
autospec=True,
return_value={automation.DOMAIN: "not valid"},
):
- await common.async_reload(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True)
assert hass.states.get("automation.hello") is None
@@ -585,8 +609,7 @@ async def test_reload_config_handles_load_fails(hass, calls):
"homeassistant.config.load_yaml_config_file",
side_effect=HomeAssistantError("bla"),
):
- await common.async_reload(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True)
assert hass.states.get("automation.hello") is not None
@@ -646,7 +669,9 @@ def running_cb(event):
autospec=True,
return_value=config,
):
- await common.async_reload(hass)
+ await hass.services.async_call(
+ automation.DOMAIN, SERVICE_RELOAD, blocking=True
+ )
hass.states.async_set(test_entity, "goodbye")
await hass.async_block_till_done()
diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py
index 4d48959632d192..3b013fad29cf39 100644
--- a/tests/components/awair/test_sensor.py
+++ b/tests/components/awair/test_sensor.py
@@ -20,6 +20,7 @@
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
+ LIGHT_LUX,
PERCENTAGE,
STATE_UNAVAILABLE,
TEMP_CELSIUS,
@@ -232,7 +233,7 @@ async def test_awair_mint_sensors(hass):
"sensor.living_room_illuminance",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}",
"441.7",
- {ATTR_UNIT_OF_MEASUREMENT: "lx"},
+ {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
)
# The Mint does not have a CO2 sensor.
@@ -290,7 +291,7 @@ async def test_awair_omni_sensors(hass):
"sensor.living_room_illuminance",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}",
"804.9",
- {ATTR_UNIT_OF_MEASUREMENT: "lx"},
+ {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
)
diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py
index e5a32c4489a296..5d9a794e3f673b 100644
--- a/tests/components/axis/test_config_flow.py
+++ b/tests/components/axis/test_config_flow.py
@@ -139,11 +139,11 @@ async def test_flow_fails_faulty_credentials(hass):
},
)
- assert result["errors"] == {"base": "faulty_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
-async def test_flow_fails_device_unavailable(hass):
- """Test that config flow fails on device unavailable."""
+async def test_flow_fails_cannot_connect(hass):
+ """Test that config flow fails on cannot connect."""
result = await hass.config_entries.flow.async_init(
AXIS_DOMAIN, context={"source": "user"}
)
@@ -165,7 +165,7 @@ async def test_flow_fails_device_unavailable(hass):
},
)
- assert result["errors"] == {"base": "device_unavailable"}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass):
diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py
index 57c7f404c7fc0b..5755a7e24e9873 100644
--- a/tests/components/bayesian/test_binary_sensor.py
+++ b/tests/components/bayesian/test_binary_sensor.py
@@ -9,7 +9,14 @@
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_RELOAD,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import Context, callback
from homeassistant.setup import async_setup_component, setup_component
from tests.async_mock import patch
@@ -686,3 +693,79 @@ async def test_reload(hass):
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))
+
+
+async def test_template_triggers(hass):
+ """Test sensor with template triggers."""
+ hass.states.async_set("input_boolean.test", STATE_OFF)
+ config = {
+ "binary_sensor": {
+ "name": "Test_Binary",
+ "platform": "bayesian",
+ "observations": [
+ {
+ "platform": "template",
+ "value_template": "{{ states.input_boolean.test.state }}",
+ "prob_given_true": 1999.9,
+ },
+ ],
+ "prior": 0.2,
+ "probability_threshold": 0.32,
+ }
+ }
+
+ await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF
+
+ events = []
+ hass.helpers.event.async_track_state_change_event(
+ "binary_sensor.test_binary", callback(lambda event: events.append(event))
+ )
+
+ context = Context()
+ hass.states.async_set("input_boolean.test", STATE_ON, context=context)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert events[0].context == context
+
+
+async def test_state_triggers(hass):
+ """Test sensor with state triggers."""
+ hass.states.async_set("sensor.test_monitored", STATE_OFF)
+
+ config = {
+ "binary_sensor": {
+ "name": "Test_Binary",
+ "platform": "bayesian",
+ "observations": [
+ {
+ "platform": "state",
+ "entity_id": "sensor.test_monitored",
+ "to_state": "off",
+ "prob_given_true": 999.9,
+ "prob_given_false": 999.4,
+ },
+ ],
+ "prior": 0.2,
+ "probability_threshold": 0.32,
+ }
+ }
+ await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF
+
+ events = []
+ hass.helpers.event.async_track_state_change_event(
+ "binary_sensor.test_binary", callback(lambda event: events.append(event))
+ )
+
+ context = Context()
+ hass.states.async_set("sensor.test_monitored", STATE_ON, context=context)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert events[0].context == context
diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py
index 4051f94c0d306b..86756c922f15f8 100644
--- a/tests/components/broadlink/__init__.py
+++ b/tests/components/broadlink/__init__.py
@@ -56,6 +56,16 @@
20025,
5,
),
+ "Kitchen": ( # Not supported.
+ "192.168.0.64",
+ "34ea34b61d2c",
+ "LB1",
+ "Broadlink",
+ "SmartBulb",
+ 0x504E,
+ 57,
+ 5,
+ ),
}
@@ -85,6 +95,9 @@ async def setup_entry(self, hass, mock_api=None, mock_entry=None):
with patch(
"homeassistant.components.broadlink.device.blk.gendevice",
return_value=mock_api,
+ ), patch(
+ "homeassistant.components.broadlink.updater.blk.discover",
+ return_value=[mock_api],
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py
index 4089c551ff5a77..bc0eba069a862b 100644
--- a/tests/components/broadlink/test_config_flow.py
+++ b/tests/components/broadlink/test_config_flow.py
@@ -12,13 +12,16 @@
from tests.async_mock import call, patch
+DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover"
+DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice"
+
@pytest.fixture(autouse=True)
def broadlink_setup_fixture():
"""Mock broadlink entry setup."""
with patch(
- "homeassistant.components.broadlink.async_setup_entry", return_value=True
- ):
+ "homeassistant.components.broadlink.async_setup", return_value=True
+ ), patch("homeassistant.components.broadlink.async_setup_entry", return_value=True):
yield
@@ -38,7 +41,7 @@ async def test_flow_user_works(hass):
assert result["step_id"] == "user"
assert result["errors"] == {}
- with patch("broadlink.discover", return_value=[mock_api]) as mock_discover:
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -69,7 +72,7 @@ async def test_flow_user_already_in_progress(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[device.get_mock_api()]):
+ with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -79,7 +82,7 @@ async def test_flow_user_already_in_progress(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[device.get_mock_api()]):
+ with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -106,7 +109,7 @@ async def test_flow_user_mac_already_configured(hass):
device.timeout = 20
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -125,7 +128,7 @@ async def test_flow_user_invalid_ip_address(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "0.0.0.1"},
@@ -142,7 +145,7 @@ async def test_flow_user_invalid_hostname(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "pancakemaster.local"},
@@ -161,7 +164,7 @@ async def test_flow_user_device_not_found(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[]):
+ with patch(DEVICE_DISCOVERY, return_value=[]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host},
@@ -172,13 +175,32 @@ async def test_flow_user_device_not_found(hass):
assert result["errors"] == {"base": "cannot_connect"}
+async def test_flow_user_device_not_supported(hass):
+ """Test we handle a device not supported in the user step."""
+ device = get_device("Kitchen")
+ mock_api = device.get_mock_api()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": device.host},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "not_supported"
+
+
async def test_flow_user_network_unreachable(hass):
"""Test we handle a network unreachable in the user step."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "192.168.1.32"},
@@ -195,7 +217,7 @@ async def test_flow_user_os_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", side_effect=OSError()):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError()):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "192.168.1.32"},
@@ -216,7 +238,7 @@ async def test_flow_auth_authentication_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -227,17 +249,17 @@ async def test_flow_auth_authentication_error(hass):
assert result["errors"] == {"base": "invalid_auth"}
-async def test_flow_auth_device_offline(hass):
- """Test we handle a device offline in the auth step."""
+async def test_flow_auth_network_timeout(hass):
+ """Test we handle a network timeout in the auth step."""
device = get_device("Living Room")
mock_api = device.get_mock_api()
- mock_api.auth.side_effect = blke.DeviceOfflineError()
+ mock_api.auth.side_effect = blke.NetworkTimeoutError()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host},
@@ -258,7 +280,7 @@ async def test_flow_auth_firmware_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host},
@@ -279,7 +301,7 @@ async def test_flow_auth_network_unreachable(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host},
@@ -300,7 +322,7 @@ async def test_flow_auth_os_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host},
@@ -321,13 +343,13 @@ async def test_flow_reset_works(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
)
- with patch("broadlink.discover", return_value=[device.get_mock_api()]):
+ with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -353,7 +375,7 @@ async def test_flow_unlock_works(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -381,18 +403,18 @@ async def test_flow_unlock_works(hass):
assert mock_api.set_lock.call_count == 1
-async def test_flow_unlock_device_offline(hass):
- """Test we handle a device offline in the unlock step."""
+async def test_flow_unlock_network_timeout(hass):
+ """Test we handle a network timeout in the unlock step."""
device = get_device("Living Room")
mock_api = device.get_mock_api()
mock_api.is_locked = True
- mock_api.set_lock.side_effect = blke.DeviceOfflineError
+ mock_api.set_lock.side_effect = blke.NetworkTimeoutError()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -419,7 +441,7 @@ async def test_flow_unlock_firmware_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -446,7 +468,7 @@ async def test_flow_unlock_network_unreachable(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -473,7 +495,7 @@ async def test_flow_unlock_os_error(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -499,7 +521,7 @@ async def test_flow_do_not_unlock(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -527,7 +549,7 @@ async def test_flow_import_works(hass):
device = get_device("Living Room")
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]) as mock_discover:
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -558,12 +580,12 @@ async def test_flow_import_already_in_progress(hass):
device = get_device("Living Room")
data = {"host": device.host}
- with patch("broadlink.discover", return_value=[device.get_mock_api()]):
+ with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data
)
- with patch("broadlink.discover", return_value=[device.get_mock_api()]):
+ with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data
)
@@ -579,7 +601,7 @@ async def test_flow_import_host_already_configured(hass):
mock_entry.add_to_hass(hass)
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -602,7 +624,7 @@ async def test_flow_import_mac_already_configured(hass):
device.host = "192.168.1.16"
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]):
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -620,7 +642,7 @@ async def test_flow_import_mac_already_configured(hass):
async def test_flow_import_device_not_found(hass):
"""Test we handle a device not found in the import step."""
- with patch("broadlink.discover", return_value=[]):
+ with patch(DEVICE_DISCOVERY, return_value=[]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -631,9 +653,25 @@ async def test_flow_import_device_not_found(hass):
assert result["reason"] == "cannot_connect"
+async def test_flow_import_device_not_supported(hass):
+ """Test we handle a device not supported in the import step."""
+ device = get_device("Kitchen")
+ mock_api = device.get_mock_api()
+
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"host": device.host},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "not_supported"
+
+
async def test_flow_import_invalid_ip_address(hass):
"""Test we handle an invalid IP address in the import step."""
- with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -646,7 +684,7 @@ async def test_flow_import_invalid_ip_address(hass):
async def test_flow_import_invalid_hostname(hass):
"""Test we handle an invalid hostname in the import step."""
- with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -659,7 +697,7 @@ async def test_flow_import_invalid_hostname(hass):
async def test_flow_import_network_unreachable(hass):
"""Test we handle a network unreachable in the import step."""
- with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -672,7 +710,7 @@ async def test_flow_import_network_unreachable(hass):
async def test_flow_import_os_error(hass):
"""Test we handle an OS error in the import step."""
- with patch("broadlink.discover", side_effect=OSError()):
+ with patch(DEVICE_DISCOVERY, side_effect=OSError()):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -692,7 +730,7 @@ async def test_flow_reauth_works(hass):
mock_api.auth.side_effect = blke.AuthenticationError()
data = {"name": device.name, **device.get_entry_data()}
- with patch("broadlink.gendevice", return_value=mock_api):
+ with patch(DEVICE_FACTORY, return_value=mock_api):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
@@ -702,7 +740,7 @@ async def test_flow_reauth_works(hass):
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]) as mock_discover:
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -728,7 +766,7 @@ async def test_flow_reauth_invalid_host(hass):
mock_api.auth.side_effect = blke.AuthenticationError()
data = {"name": device.name, **device.get_entry_data()}
- with patch("broadlink.gendevice", return_value=mock_api):
+ with patch(DEVICE_FACTORY, return_value=mock_api):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
@@ -736,7 +774,7 @@ async def test_flow_reauth_invalid_host(hass):
device.mac = get_device("Office").mac
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]) as mock_discover:
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
@@ -762,7 +800,7 @@ async def test_flow_reauth_valid_host(hass):
mock_api.auth.side_effect = blke.AuthenticationError()
data = {"name": device.name, **device.get_entry_data()}
- with patch("broadlink.gendevice", return_value=mock_api):
+ with patch(DEVICE_FACTORY, return_value=mock_api):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
@@ -770,7 +808,7 @@ async def test_flow_reauth_valid_host(hass):
device.host = "192.168.1.128"
mock_api = device.get_mock_api()
- with patch("broadlink.discover", return_value=[mock_api]) as mock_discover:
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": device.host, "timeout": device.timeout},
diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py
index 5cd0457b55269b..d267243aeb9481 100644
--- a/tests/components/broadlink/test_device.py
+++ b/tests/components/broadlink/test_device.py
@@ -20,16 +20,13 @@
async def test_device_setup(hass):
"""Test a successful setup."""
device = get_device("Office")
- mock_api = device.get_mock_api()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass)
assert mock_entry.state == ENTRY_STATE_LOADED
assert mock_api.auth.call_count == 1
@@ -46,15 +43,13 @@ async def test_device_setup_authentication_error(hass):
device = get_device("Living Room")
mock_api = device.get_mock_api()
mock_api.auth.side_effect = blke.AuthenticationError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_ERROR
assert mock_api.auth.call_count == 1
@@ -67,20 +62,18 @@ async def test_device_setup_authentication_error(hass):
}
-async def test_device_setup_device_offline(hass):
- """Test we handle a device offline."""
+async def test_device_setup_network_timeout(hass):
+ """Test we handle a network timeout."""
device = get_device("Office")
mock_api = device.get_mock_api()
- mock_api.auth.side_effect = blke.DeviceOfflineError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
+ mock_api.auth.side_effect = blke.NetworkTimeoutError()
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_RETRY
assert mock_api.auth.call_count == 1
@@ -93,15 +86,13 @@ async def test_device_setup_os_error(hass):
device = get_device("Office")
mock_api = device.get_mock_api()
mock_api.auth.side_effect = OSError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_RETRY
assert mock_api.auth.call_count == 1
@@ -114,15 +105,13 @@ async def test_device_setup_broadlink_exception(hass):
device = get_device("Office")
mock_api = device.get_mock_api()
mock_api.auth.side_effect = blke.BroadlinkException()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_ERROR
assert mock_api.auth.call_count == 1
@@ -130,20 +119,18 @@ async def test_device_setup_broadlink_exception(hass):
assert mock_init.call_count == 0
-async def test_device_setup_update_device_offline(hass):
- """Test we handle a device offline in the update step."""
+async def test_device_setup_update_network_timeout(hass):
+ """Test we handle a network timeout in the update step."""
device = get_device("Office")
mock_api = device.get_mock_api()
- mock_api.check_sensors.side_effect = blke.DeviceOfflineError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
+ mock_api.check_sensors.side_effect = blke.NetworkTimeoutError()
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_RETRY
assert mock_api.auth.call_count == 1
@@ -157,15 +144,13 @@ async def test_device_setup_update_authorization_error(hass):
device = get_device("Office")
mock_api = device.get_mock_api()
mock_api.check_sensors.side_effect = (blke.AuthorizationError(), None)
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_LOADED
assert mock_api.auth.call_count == 2
@@ -179,19 +164,17 @@ async def test_device_setup_update_authorization_error(hass):
async def test_device_setup_update_authentication_error(hass):
"""Test we handle an authentication error in the update step."""
- device = get_device("Living Room")
+ device = get_device("Garage")
mock_api = device.get_mock_api()
mock_api.check_sensors.side_effect = blke.AuthorizationError()
mock_api.auth.side_effect = (None, blke.AuthenticationError())
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_RETRY
assert mock_api.auth.call_count == 2
@@ -207,18 +190,16 @@ async def test_device_setup_update_authentication_error(hass):
async def test_device_setup_update_broadlink_exception(hass):
"""Test we handle a Broadlink exception in the update step."""
- device = get_device("Living Room")
+ device = get_device("Garage")
mock_api = device.get_mock_api()
mock_api.check_sensors.side_effect = blke.BroadlinkException()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
+ with patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward, patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_SETUP_RETRY
assert mock_api.auth.call_count == 1
@@ -232,13 +213,9 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass):
device = get_device("Office")
mock_api = device.get_mock_api()
mock_api.get_fwversion.side_effect = blke.BroadlinkException()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ) as mock_forward:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward:
+ mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_LOADED
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
@@ -252,13 +229,9 @@ async def test_device_setup_get_fwversion_os_error(hass):
device = get_device("Office")
mock_api = device.get_mock_api()
mock_api.get_fwversion.side_effect = OSError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ) as mock_forward:
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward:
+ _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
assert mock_entry.state == ENTRY_STATE_LOADED
forward_entries = {c[1][1] for c in mock_forward.mock_calls}
@@ -270,16 +243,12 @@ async def test_device_setup_get_fwversion_os_error(hass):
async def test_device_setup_registry(hass):
"""Test we register the device and the entries correctly."""
device = get_device("Office")
- mock_api = device.get_mock_api()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- with patch("broadlink.gendevice", return_value=mock_api):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ _, mock_entry = await device.setup_entry(hass)
+ await hass.async_block_till_done()
assert len(device_registry.devices) == 1
@@ -299,14 +268,9 @@ async def test_device_setup_registry(hass):
async def test_device_unload_works(hass):
"""Test we unload the device."""
device = get_device("Office")
- mock_api = device.get_mock_api()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ with patch.object(hass.config_entries, "async_forward_entry_setup"):
+ mock_api, mock_entry = await device.setup_entry(hass)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
@@ -325,13 +289,11 @@ async def test_device_unload_authentication_error(hass):
device = get_device("Living Room")
mock_api = device.get_mock_api()
mock_api.auth.side_effect = blke.AuthenticationError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ), patch.object(hass.config_entries.flow, "async_init"):
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object(
+ hass.config_entries.flow, "async_init"
+ ):
+ _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
@@ -346,14 +308,10 @@ async def test_device_unload_update_failed(hass):
"""Test we unload a device that failed the update step."""
device = get_device("Office")
mock_api = device.get_mock_api()
- mock_api.check_sensors.side_effect = blke.DeviceOfflineError()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
+ mock_api.check_sensors.side_effect = blke.NetworkTimeoutError()
- with patch("broadlink.gendevice", return_value=mock_api), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
+ with patch.object(hass.config_entries, "async_forward_entry_setup"):
+ _, mock_entry = await device.setup_entry(hass, mock_api=mock_api)
with patch.object(
hass.config_entries, "async_forward_entry_unload", return_value=True
@@ -367,17 +325,16 @@ async def test_device_unload_update_failed(hass):
async def test_device_update_listener(hass):
"""Test we update device and entity registry when the entry is renamed."""
device = get_device("Office")
- mock_api = device.get_mock_api()
- mock_entry = device.get_mock_entry()
- mock_entry.add_to_hass(hass)
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
- with patch("broadlink.gendevice", return_value=mock_api):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ mock_api, mock_entry = await device.setup_entry(hass)
+ await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api
+ ):
hass.config_entries.async_update_entry(mock_entry, title="New Name")
await hass.async_block_till_done()
diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py
index 06e58b83522e18..153c07deeac541 100644
--- a/tests/components/brother/test_config_flow.py
+++ b/tests/components/brother/test_config_flow.py
@@ -76,7 +76,7 @@ async def test_connection_error(hass):
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_snmp_error(hass):
@@ -125,7 +125,7 @@ async def test_zeroconf_no_data(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
async def test_zeroconf_not_brother_printer_error(hass):
@@ -156,7 +156,7 @@ async def test_zeroconf_snmp_error(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
async def test_zeroconf_device_exists_abort(hass):
diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py
index 45b0f16c0a10e7..511b566ce417fe 100644
--- a/tests/components/bsblan/__init__.py
+++ b/tests/components/bsblan/__init__.py
@@ -5,7 +5,7 @@
CONF_PASSKEY,
DOMAIN,
)
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@@ -23,7 +23,7 @@ async def init_integration(
"http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
params={"Parameter": "6224,6225,6226"},
text=load_fixture("bsblan/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py
index c24fbc34f6a24a..4c04db012ba35f 100644
--- a/tests/components/bsblan/test_config_flow.py
+++ b/tests/components/bsblan/test_config_flow.py
@@ -5,7 +5,7 @@
from homeassistant.components.bsblan import config_flow
from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY
from homeassistant.config_entries import SOURCE_USER
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -40,7 +40,7 @@ async def test_connection_error(
data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80},
)
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
assert result["step_id"] == "user"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -67,7 +67,7 @@ async def test_full_user_flow_implementation(
aioclient_mock.post(
"http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
text=load_fixture("bsblan/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py
index c3db5067b1ff0b..966adc97b6715a 100644
--- a/tests/components/camera/test_init.py
+++ b/tests/components/camera/test_init.py
@@ -14,7 +14,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
-from tests.async_mock import PropertyMock, mock_open, patch
+from tests.async_mock import Mock, PropertyMock, mock_open, patch
from tests.components.camera import common
@@ -114,8 +114,9 @@ async def test_snapshot_service(hass, mock_camera):
"""Test snapshot service."""
mopen = mock_open()
- with patch(
- "homeassistant.components.camera.open", mopen, create=True
+ with patch("homeassistant.components.camera.open", mopen, create=True), patch(
+ "homeassistant.components.camera.os.path.exists",
+ Mock(spec="os.path.exists", return_value=True),
), patch.object(hass.config, "is_allowed_path", return_value=True):
await hass.services.async_call(
camera.DOMAIN,
diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py
index cc85edd806a9c4..9d0e488d51686f 100644
--- a/tests/components/canary/__init__.py
+++ b/tests/components/canary/__init__.py
@@ -1 +1,116 @@
-"""Tests for the canary component."""
+"""Tests for the Canary integration."""
+from unittest.mock import MagicMock, PropertyMock
+
+from canary.api import SensorType
+
+from homeassistant.components.canary.const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+ENTRY_CONFIG = {
+ CONF_PASSWORD: "test-password",
+ CONF_USERNAME: "test-username",
+}
+
+ENTRY_OPTIONS = {
+ CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS,
+ CONF_TIMEOUT: DEFAULT_TIMEOUT,
+}
+
+USER_INPUT = {
+ CONF_PASSWORD: "test-password",
+ CONF_USERNAME: "test-username",
+}
+
+YAML_CONFIG = {
+ CONF_PASSWORD: "test-password",
+ CONF_USERNAME: "test-username",
+ CONF_TIMEOUT: 5,
+}
+
+
+def _patch_async_setup(return_value=True):
+ return patch(
+ "homeassistant.components.canary.async_setup",
+ return_value=return_value,
+ )
+
+
+def _patch_async_setup_entry(return_value=True):
+ return patch(
+ "homeassistant.components.canary.async_setup_entry",
+ return_value=return_value,
+ )
+
+
+async def init_integration(
+ hass: HomeAssistantType,
+ *,
+ data: dict = ENTRY_CONFIG,
+ options: dict = ENTRY_OPTIONS,
+ skip_entry_setup: bool = False,
+) -> MockConfigEntry:
+ """Set up the Canary integration in Home Assistant."""
+ entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
+ entry.add_to_hass(hass)
+
+ if not skip_entry_setup:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
+
+
+def mock_device(device_id, name, is_online=True, device_type_name=None):
+ """Mock Canary Device class."""
+ device = MagicMock()
+ type(device).device_id = PropertyMock(return_value=device_id)
+ type(device).name = PropertyMock(return_value=name)
+ type(device).is_online = PropertyMock(return_value=is_online)
+ type(device).device_type = PropertyMock(
+ return_value={"id": 1, "name": device_type_name}
+ )
+
+ return device
+
+
+def mock_location(
+ location_id, name, is_celsius=True, devices=None, mode=None, is_private=False
+):
+ """Mock Canary Location class."""
+ location = MagicMock()
+ type(location).location_id = PropertyMock(return_value=location_id)
+ type(location).name = PropertyMock(return_value=name)
+ type(location).is_celsius = PropertyMock(return_value=is_celsius)
+ type(location).is_private = PropertyMock(return_value=is_private)
+ type(location).devices = PropertyMock(return_value=devices or [])
+ type(location).mode = PropertyMock(return_value=mode)
+
+ return location
+
+
+def mock_mode(mode_id, name):
+ """Mock Canary Mode class."""
+ mode = MagicMock()
+ type(mode).mode_id = PropertyMock(return_value=mode_id)
+ type(mode).name = PropertyMock(return_value=name)
+ type(mode).resource_url = PropertyMock(return_value=f"/v1/modes/{mode_id}")
+
+ return mode
+
+
+def mock_reading(sensor_type, sensor_value):
+ """Mock Canary Reading class."""
+ reading = MagicMock()
+ type(reading).sensor_type = SensorType(sensor_type)
+ type(reading).value = PropertyMock(return_value=sensor_value)
+
+ return reading
diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py
new file mode 100644
index 00000000000000..01527a193c03a3
--- /dev/null
+++ b/tests/components/canary/conftest.py
@@ -0,0 +1,53 @@
+"""Define fixtures available for all tests."""
+from canary.api import Api
+from pytest import fixture
+
+from tests.async_mock import MagicMock, patch
+
+
+@fixture
+def canary(hass):
+ """Mock the CanaryApi for easier testing."""
+ with patch.object(Api, "login", return_value=True), patch(
+ "homeassistant.components.canary.Api"
+ ) as mock_canary:
+ instance = mock_canary.return_value = Api(
+ "test-username",
+ "test-password",
+ 1,
+ )
+
+ instance.login = MagicMock(return_value=True)
+ instance.get_entries = MagicMock(return_value=[])
+ instance.get_locations = MagicMock(return_value=[])
+ instance.get_location = MagicMock(return_value=None)
+ instance.get_modes = MagicMock(return_value=[])
+ instance.get_readings = MagicMock(return_value=[])
+ instance.get_latest_readings = MagicMock(return_value=[])
+ instance.set_location_mode = MagicMock(return_value=None)
+
+ yield mock_canary
+
+
+@fixture
+def canary_config_flow(hass):
+ """Mock the CanaryApi for easier config flow testing."""
+ with patch.object(Api, "login", return_value=True), patch(
+ "homeassistant.components.canary.config_flow.Api"
+ ) as mock_canary:
+ instance = mock_canary.return_value = Api(
+ "test-username",
+ "test-password",
+ 1,
+ )
+
+ instance.login = MagicMock(return_value=True)
+ instance.get_entries = MagicMock(return_value=[])
+ instance.get_locations = MagicMock(return_value=[])
+ instance.get_location = MagicMock(return_value=None)
+ instance.get_modes = MagicMock(return_value=[])
+ instance.get_readings = MagicMock(return_value=[])
+ instance.get_latest_readings = MagicMock(return_value=[])
+ instance.set_location_mode = MagicMock(return_value=None)
+
+ yield mock_canary
diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py
new file mode 100644
index 00000000000000..930fd9613e0465
--- /dev/null
+++ b/tests/components/canary/test_alarm_control_panel.py
@@ -0,0 +1,167 @@
+"""The tests for the Canary alarm_control_panel platform."""
+from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
+
+from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
+from homeassistant.components.canary import DOMAIN
+from homeassistant.const import (
+ SERVICE_ALARM_ARM_AWAY,
+ SERVICE_ALARM_ARM_HOME,
+ SERVICE_ALARM_ARM_NIGHT,
+ SERVICE_ALARM_DISARM,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_UNKNOWN,
+)
+from homeassistant.setup import async_setup_component
+
+from . import mock_device, mock_location, mock_mode
+
+from tests.async_mock import PropertyMock, patch
+from tests.common import mock_registry
+
+
+async def test_alarm_control_panel(hass, canary) -> None:
+ """Test the creation and values of the alarm_control_panel for Canary."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ registry = mock_registry(hass)
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
+
+ mocked_location = mock_location(
+ location_id=100,
+ name="Home",
+ is_celsius=True,
+ is_private=False,
+ mode=mock_mode(7, "standby"),
+ devices=[online_device_at_home],
+ )
+
+ instance = canary.return_value
+ instance.get_locations.return_value = [mocked_location]
+
+ config = {DOMAIN: {"username": "test-username", "password": "test-password"}}
+ with patch("homeassistant.components.canary.PLATFORMS", ["alarm_control_panel"]):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ entity_id = "alarm_control_panel.home"
+ entity_entry = registry.async_get(entity_id)
+ assert entity_entry
+ assert entity_entry.unique_id == "100"
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_UNKNOWN
+ assert not state.attributes["private"]
+
+ # test private system
+ type(mocked_location).is_private = PropertyMock(return_value=True)
+
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ALARM_DISARMED
+ assert state.attributes["private"]
+
+ type(mocked_location).is_private = PropertyMock(return_value=False)
+
+ # test armed home
+ type(mocked_location).mode = PropertyMock(
+ return_value=mock_mode(4, LOCATION_MODE_HOME)
+ )
+
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ALARM_ARMED_HOME
+
+ # test armed away
+ type(mocked_location).mode = PropertyMock(
+ return_value=mock_mode(5, LOCATION_MODE_AWAY)
+ )
+
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ALARM_ARMED_AWAY
+
+ # test armed night
+ type(mocked_location).mode = PropertyMock(
+ return_value=mock_mode(6, LOCATION_MODE_NIGHT)
+ )
+
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ALARM_ARMED_NIGHT
+
+
+async def test_alarm_control_panel_services(hass, canary) -> None:
+ """Test the services of the alarm_control_panel for Canary."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
+
+ mocked_location = mock_location(
+ location_id=100,
+ name="Home",
+ is_celsius=True,
+ mode=mock_mode(1, "disarmed"),
+ devices=[online_device_at_home],
+ )
+
+ instance = canary.return_value
+ instance.get_locations.return_value = [mocked_location]
+
+ config = {DOMAIN: {"username": "test-username", "password": "test-password"}}
+ with patch("homeassistant.components.canary.PLATFORMS", ["alarm_control_panel"]):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ entity_id = "alarm_control_panel.home"
+
+ # test arm away
+ await hass.services.async_call(
+ ALARM_DOMAIN,
+ SERVICE_ALARM_ARM_AWAY,
+ service_data={"entity_id": entity_id},
+ blocking=True,
+ )
+ instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY)
+
+ # test arm home
+ await hass.services.async_call(
+ ALARM_DOMAIN,
+ SERVICE_ALARM_ARM_HOME,
+ service_data={"entity_id": entity_id},
+ blocking=True,
+ )
+ instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME)
+
+ # test arm night
+ await hass.services.async_call(
+ ALARM_DOMAIN,
+ SERVICE_ALARM_ARM_NIGHT,
+ service_data={"entity_id": entity_id},
+ blocking=True,
+ )
+ instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT)
+
+ # test disarm
+ await hass.services.async_call(
+ ALARM_DOMAIN,
+ SERVICE_ALARM_DISARM,
+ service_data={"entity_id": entity_id},
+ blocking=True,
+ )
+ instance.set_location_mode.assert_called_with(100, "disarmed", True)
diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py
new file mode 100644
index 00000000000000..36c6990a6633db
--- /dev/null
+++ b/tests/components/canary/test_config_flow.py
@@ -0,0 +1,127 @@
+"""Test the Canary config flow."""
+from requests import ConnectTimeout, HTTPError
+
+from homeassistant.components.canary.const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_TIMEOUT
+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 USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration
+
+from tests.async_mock import patch
+
+
+async def test_user_form(hass, canary_config_flow):
+ """Test we get the user initiated form."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
+ 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 result["title"] == "test-username"
+ assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT}
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_user_form_cannot_connect(hass, canary_config_flow):
+ """Test we handle errors that should trigger the cannot connect error."""
+ canary_config_flow.side_effect = HTTPError()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT,
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ canary_config_flow.side_effect = ConnectTimeout()
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT,
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_user_form_unexpected_exception(hass, canary_config_flow):
+ """Test we handle unexpected exception."""
+ canary_config_flow.side_effect = Exception()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT,
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_user_form_single_instance_allowed(hass, canary_config_flow):
+ """Test that configuring more than one instance is rejected."""
+ await init_integration(hass, skip_entry_setup=True)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=USER_INPUT,
+ )
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_options_flow(hass):
+ """Test updating options."""
+ with patch("homeassistant.components.canary.PLATFORMS", []):
+ entry = await init_integration(hass)
+
+ assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS
+ assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ with _patch_async_setup(), _patch_async_setup_entry():
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_FFMPEG_ARGUMENTS: "-v", CONF_TIMEOUT: 7},
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v"
+ assert result["data"][CONF_TIMEOUT] == 7
diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py
index 0cfbfd56de65ff..f548a007505d4e 100644
--- a/tests/components/canary/test_init.py
+++ b/tests/components/canary/test_init.py
@@ -1,73 +1,82 @@
"""The tests for the Canary component."""
-import unittest
-
-from homeassistant import setup
-import homeassistant.components.canary as canary
-
-from tests.async_mock import MagicMock, PropertyMock, patch
-from tests.common import get_test_home_assistant
-
-
-def mock_device(device_id, name, is_online=True, device_type_name=None):
- """Mock Canary Device class."""
- device = MagicMock()
- type(device).device_id = PropertyMock(return_value=device_id)
- type(device).name = PropertyMock(return_value=name)
- type(device).is_online = PropertyMock(return_value=is_online)
- type(device).device_type = PropertyMock(
- return_value={"id": 1, "name": device_type_name}
- )
- return device
-
-
-def mock_location(name, is_celsius=True, devices=None):
- """Mock Canary Location class."""
- location = MagicMock()
- type(location).name = PropertyMock(return_value=name)
- type(location).is_celsius = PropertyMock(return_value=is_celsius)
- type(location).devices = PropertyMock(return_value=devices or [])
- return location
-
-
-def mock_reading(sensor_type, sensor_value):
- """Mock Canary Reading class."""
- reading = MagicMock()
- type(reading).sensor_type = PropertyMock(return_value=sensor_type)
- type(reading).value = PropertyMock(return_value=sensor_value)
- return reading
-
-
-class TestCanary(unittest.TestCase):
- """Tests the Canary component."""
-
- def setUp(self):
- """Initialize values for this test case class."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- @patch("homeassistant.components.canary.CanaryData.update")
- @patch("canary.api.Api.login")
- def test_setup_with_valid_config(self, mock_login, mock_update):
- """Test setup component."""
- config = {"canary": {"username": "foo@bar.org", "password": "bar"}}
-
- assert setup.setup_component(self.hass, canary.DOMAIN, config)
-
- mock_update.assert_called_once_with()
- mock_login.assert_called_once_with()
-
- def test_setup_with_missing_password(self):
- """Test setup component."""
- config = {"canary": {"username": "foo@bar.org"}}
-
- assert not setup.setup_component(self.hass, canary.DOMAIN, config)
-
- def test_setup_with_missing_username(self):
- """Test setup component."""
- config = {"canary": {"password": "bar"}}
-
- assert not setup.setup_component(self.hass, canary.DOMAIN, config)
+from requests import ConnectTimeout
+
+from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
+from homeassistant.components.canary.const import CONF_FFMPEG_ARGUMENTS, DOMAIN
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
+from homeassistant.setup import async_setup_component
+
+from . import YAML_CONFIG, init_integration
+
+from tests.async_mock import patch
+
+
+async def test_import_from_yaml(hass, canary) -> None:
+ """Test import from YAML."""
+ with patch(
+ "homeassistant.components.canary.async_setup_entry",
+ return_value=True,
+ ):
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: YAML_CONFIG})
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+
+ assert entries[0].data[CONF_USERNAME] == "test-username"
+ assert entries[0].data[CONF_PASSWORD] == "test-password"
+ assert entries[0].data[CONF_TIMEOUT] == 5
+
+
+async def test_import_from_yaml_ffmpeg(hass, canary) -> None:
+ """Test import from YAML with ffmpeg arguments."""
+ with patch(
+ "homeassistant.components.canary.async_setup_entry",
+ return_value=True,
+ ):
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: YAML_CONFIG,
+ CAMERA_DOMAIN: [{"platform": DOMAIN, CONF_FFMPEG_ARGUMENTS: "-v"}],
+ },
+ )
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+
+ assert entries[0].data[CONF_USERNAME] == "test-username"
+ assert entries[0].data[CONF_PASSWORD] == "test-password"
+ assert entries[0].data[CONF_TIMEOUT] == 5
+ assert entries[0].data.get(CONF_FFMPEG_ARGUMENTS) == "-v"
+
+
+async def test_unload_entry(hass, canary):
+ """Test successful unload of entry."""
+ entry = await init_integration(hass)
+
+ assert entry
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == ENTRY_STATE_LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ assert not hass.data.get(DOMAIN)
+
+
+async def test_async_setup_raises_entry_not_ready(hass, canary):
+ """Test that it throws ConfigEntryNotReady when exception occurs during setup."""
+ canary.side_effect = ConnectTimeout()
+
+ entry = await init_integration(hass)
+ assert entry
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py
index f328fb5a976014..d32741d3705e71 100644
--- a/tests/components/canary/test_sensor.py
+++ b/tests/components/canary/test_sensor.py
@@ -1,203 +1,213 @@
"""The tests for the Canary sensor platform."""
-import copy
-import unittest
+from datetime import timedelta
-from homeassistant.components.canary import DATA_CANARY, sensor as canary
+from homeassistant.components.canary.const import DOMAIN, MANUFACTURER
from homeassistant.components.canary.sensor import (
ATTR_AIR_QUALITY,
- SENSOR_TYPES,
STATE_AIR_QUALITY_ABNORMAL,
STATE_AIR_QUALITY_NORMAL,
STATE_AIR_QUALITY_VERY_ABNORMAL,
- CanarySensor,
)
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
-
-from tests.async_mock import Mock
-from tests.common import get_test_home_assistant
-from tests.components.canary.test_init import mock_device, mock_location
-
-VALID_CONFIG = {"canary": {"username": "foo@bar.org", "password": "bar"}}
-
-
-class TestCanarySensorSetup(unittest.TestCase):
- """Test the Canary platform."""
-
- DEVICES = []
-
- def add_entities(self, devices, action):
- """Mock add devices."""
- for device in devices:
- self.DEVICES.append(device)
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.config = copy.deepcopy(VALID_CONFIG)
- self.addCleanup(self.hass.stop)
-
- def test_setup_sensors(self):
- """Test the sensor setup."""
- online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
- offline_device_at_home = mock_device(21, "Front Yard", False, "Canary Pro")
- online_device_at_work = mock_device(22, "Office", True, "Canary Pro")
-
- self.hass.data[DATA_CANARY] = Mock()
- self.hass.data[DATA_CANARY].locations = [
- mock_location(
- "Home", True, devices=[online_device_at_home, offline_device_at_home]
- ),
- mock_location("Work", True, devices=[online_device_at_work]),
- ]
-
- canary.setup_platform(self.hass, self.config, self.add_entities, None)
-
- assert len(self.DEVICES) == 6
-
- def test_temperature_sensor(self):
- """Test temperature sensor with fahrenheit."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home", False)
-
- data = Mock()
- data.get_reading.return_value = 21.1234
-
- sensor = CanarySensor(data, SENSOR_TYPES[0], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Temperature"
- assert sensor.unit_of_measurement == TEMP_CELSIUS
- assert sensor.state == 21.12
- assert sensor.icon == "mdi:thermometer"
-
- def test_temperature_sensor_with_none_sensor_value(self):
- """Test temperature sensor with fahrenheit."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home", False)
-
- data = Mock()
- data.get_reading.return_value = None
-
- sensor = CanarySensor(data, SENSOR_TYPES[0], location, device)
- sensor.update()
-
- assert sensor.state is None
-
- def test_humidity_sensor(self):
- """Test humidity sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = 50.4567
-
- sensor = CanarySensor(data, SENSOR_TYPES[1], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Humidity"
- assert sensor.unit_of_measurement == PERCENTAGE
- assert sensor.state == 50.46
- assert sensor.icon == "mdi:water-percent"
-
- def test_air_quality_sensor_with_very_abnormal_reading(self):
- """Test air quality sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = 0.4
-
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Air Quality"
- assert sensor.unit_of_measurement is None
- assert sensor.state == 0.4
- assert sensor.icon == "mdi:weather-windy"
-
- air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
- assert air_quality == STATE_AIR_QUALITY_VERY_ABNORMAL
-
- def test_air_quality_sensor_with_abnormal_reading(self):
- """Test air quality sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = 0.59
-
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Air Quality"
- assert sensor.unit_of_measurement is None
- assert sensor.state == 0.59
- assert sensor.icon == "mdi:weather-windy"
-
- air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
- assert air_quality == STATE_AIR_QUALITY_ABNORMAL
-
- def test_air_quality_sensor_with_normal_reading(self):
- """Test air quality sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = 1.0
-
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Air Quality"
- assert sensor.unit_of_measurement is None
- assert sensor.state == 1.0
- assert sensor.icon == "mdi:weather-windy"
-
- air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
- assert air_quality == STATE_AIR_QUALITY_NORMAL
-
- def test_air_quality_sensor_with_none_sensor_value(self):
- """Test air quality sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = None
-
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
-
- assert sensor.state is None
- assert sensor.device_state_attributes is None
-
- def test_battery_sensor(self):
- """Test battery sensor."""
- device = mock_device(10, "Family Room", "Canary Flex")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = 70.4567
-
- sensor = CanarySensor(data, SENSOR_TYPES[4], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Battery"
- assert sensor.unit_of_measurement == PERCENTAGE
- assert sensor.state == 70.46
- assert sensor.icon == "mdi:battery-70"
-
- def test_wifi_sensor(self):
- """Test battery sensor."""
- device = mock_device(10, "Family Room", "Canary Flex")
- location = mock_location("Home")
-
- data = Mock()
- data.get_reading.return_value = -57
-
- sensor = CanarySensor(data, SENSOR_TYPES[3], location, device)
- sensor.update()
-
- assert sensor.name == "Home Family Room Wifi"
- assert sensor.unit_of_measurement == "dBm"
- assert sensor.state == -57
- assert sensor.icon == "mdi:wifi"
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ DEVICE_CLASS_TEMPERATURE,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ TEMP_CELSIUS,
+)
+from homeassistant.setup import async_setup_component
+from homeassistant.util.dt import utcnow
+
+from . import mock_device, mock_location, mock_reading
+
+from tests.async_mock import patch
+from tests.common import async_fire_time_changed, mock_device_registry, mock_registry
+
+
+async def test_sensors_pro(hass, canary) -> None:
+ """Test the creation and values of the sensors for Canary Pro."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
+
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
+
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
+
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.59"),
+ ]
+
+ config = {DOMAIN: {"username": "test-username", "password": "test-password"}}
+ with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ sensors = {
+ "home_dining_room_temperature": (
+ "20_temperature",
+ "21.12",
+ TEMP_CELSIUS,
+ DEVICE_CLASS_TEMPERATURE,
+ None,
+ ),
+ "home_dining_room_humidity": (
+ "20_humidity",
+ "50.46",
+ PERCENTAGE,
+ DEVICE_CLASS_HUMIDITY,
+ None,
+ ),
+ "home_dining_room_air_quality": (
+ "20_air_quality",
+ "0.59",
+ None,
+ None,
+ "mdi:weather-windy",
+ ),
+ }
+
+ for (sensor_id, data) in sensors.items():
+ entity_entry = registry.async_get(f"sensor.{sensor_id}")
+ assert entity_entry
+ assert entity_entry.device_class == data[3]
+ assert entity_entry.unique_id == data[0]
+ assert entity_entry.original_icon == data[4]
+
+ state = hass.states.get(f"sensor.{sensor_id}")
+ assert state
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2]
+ assert state.state == data[1]
+
+ device = device_registry.async_get_device({(DOMAIN, "20")}, set())
+ assert device
+ assert device.manufacturer == MANUFACTURER
+ assert device.name == "Dining Room"
+ assert device.model == "Canary Pro"
+
+
+async def test_sensors_attributes_pro(hass, canary) -> None:
+ """Test the creation and values of the sensors attributes for Canary Pro."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
+
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
+
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.59"),
+ ]
+
+ config = {DOMAIN: {"username": "test-username", "password": "test-password"}}
+ with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ entity_id = "sensor.home_dining_room_air_quality"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL
+
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.4"),
+ ]
+
+ future = utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL
+
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "1.0"),
+ ]
+
+ future += timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL
+
+
+async def test_sensors_flex(hass, canary) -> None:
+ """Test the creation and values of the sensors for Canary Flex."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
+
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Flex")
+
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
+
+ instance.get_latest_readings.return_value = [
+ mock_reading("battery", "70.4567"),
+ mock_reading("wifi", "-57"),
+ ]
+
+ config = {DOMAIN: {"username": "test-username", "password": "test-password"}}
+ with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ sensors = {
+ "home_dining_room_battery": (
+ "20_battery",
+ "70.46",
+ PERCENTAGE,
+ DEVICE_CLASS_BATTERY,
+ None,
+ ),
+ "home_dining_room_wifi": (
+ "20_wifi",
+ "-57.0",
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ None,
+ ),
+ }
+
+ for (sensor_id, data) in sensors.items():
+ entity_entry = registry.async_get(f"sensor.{sensor_id}")
+ assert entity_entry
+ assert entity_entry.device_class == data[3]
+ assert entity_entry.unique_id == data[0]
+ assert entity_entry.original_icon == data[4]
+
+ state = hass.states.get(f"sensor.{sensor_id}")
+ assert state
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2]
+ assert state.state == data[1]
+
+ device = device_registry.async_get_device({(DOMAIN, "20")}, set())
+ assert device
+ assert device.manufacturer == MANUFACTURER
+ assert device.name == "Dining Room"
+ assert device.model == "Canary Flex"
diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py
index 2842ddc23f07e7..2fff760bb7055e 100644
--- a/tests/components/cast/test_home_assistant_cast.py
+++ b/tests/components/cast/test_home_assistant_cast.py
@@ -1,4 +1,5 @@
"""Test Home Assistant Cast."""
+
from homeassistant.components.cast import home_assistant_cast
from homeassistant.config import async_process_ha_core_config
@@ -6,7 +7,7 @@
from tests.common import MockConfigEntry, async_mock_signal
-async def test_service_show_view(hass):
+async def test_service_show_view(hass, mock_zeroconf):
"""Test we don't set app id in prod."""
await async_process_ha_core_config(
hass,
@@ -33,7 +34,7 @@ async def test_service_show_view(hass):
assert url_path is None
-async def test_service_show_view_dashboard(hass):
+async def test_service_show_view_dashboard(hass, mock_zeroconf):
"""Test casting a specific dashboard."""
await async_process_ha_core_config(
hass,
@@ -60,7 +61,7 @@ async def test_service_show_view_dashboard(hass):
assert url_path == "mock-dashboard"
-async def test_use_cloud_url(hass):
+async def test_use_cloud_url(hass, mock_zeroconf):
"""Test that we fall back to cloud url."""
await async_process_ha_core_config(
hass,
diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py
index d0d9c4b25b7c5d..aaabca71885803 100644
--- a/tests/components/cloud/test_client.py
+++ b/tests/components/cloud/test_client.py
@@ -5,6 +5,7 @@
from homeassistant.components.cloud import DOMAIN
from homeassistant.components.cloud.client import CloudClient
from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE
+from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import State
from homeassistant.setup import async_setup_component
@@ -175,7 +176,7 @@ async def handler(hass, webhook_id, request):
{
"cloudhook_id": "mock-cloud-id",
"body": '{"hello": "world"}',
- "headers": {"content-type": "application/json"},
+ "headers": {"content-type": CONTENT_TYPE_JSON},
"method": "POST",
"query": None,
}
@@ -184,7 +185,7 @@ async def handler(hass, webhook_id, request):
assert response == {
"status": 200,
"body": '{"from": "handler"}',
- "headers": {"Content-Type": "application/json"},
+ "headers": {"Content-Type": CONTENT_TYPE_JSON},
}
assert len(received) == 1
@@ -197,7 +198,7 @@ async def handler(hass, webhook_id, request):
{
"cloudhook_id": "mock-nonexisting-id",
"body": '{"nonexisting": "payload"}',
- "headers": {"content-type": "application/json"},
+ "headers": {"content-type": CONTENT_TYPE_JSON},
"method": "POST",
"query": None,
}
diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py
index a9d9c61444a9bb..c6d315b05b59e1 100644
--- a/tests/components/command_line/test_switch.py
+++ b/tests/components/command_line/test_switch.py
@@ -2,209 +2,240 @@
import json
import os
import tempfile
-import unittest
import homeassistant.components.command_line.switch as command_line
import homeassistant.components.switch as switch
-from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
-from tests.components.switch import common
-
-
-# pylint: disable=invalid-name
-class TestCommandSwitch(unittest.TestCase):
- """Test the command switch."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- def test_state_none(self):
- """Test with none state."""
- with tempfile.TemporaryDirectory() as tempdirname:
- path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- }
- assert setup_component(
- self.hass,
- switch.DOMAIN,
- {
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
- }
- },
- )
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
-
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- def test_state_value(self):
- """Test with state value."""
- with tempfile.TemporaryDirectory() as tempdirname:
- path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- "value_template": '{{ value=="1" }}',
- }
- assert setup_component(
- self.hass,
- switch.DOMAIN,
- {
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
- }
- },
- )
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
-
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- def test_state_json_value(self):
- """Test with state JSON value."""
- with tempfile.TemporaryDirectory() as tempdirname:
- path = os.path.join(tempdirname, "switch_status")
- oncmd = json.dumps({"status": "ok"})
- offcmd = json.dumps({"status": "nope"})
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo '{oncmd}' > {path}",
- "command_off": f"echo '{offcmd}' > {path}",
- "value_template": '{{ value_json.status=="ok" }}',
- }
- assert setup_component(
- self.hass,
- switch.DOMAIN,
- {
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
- }
- },
- )
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
-
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- def test_state_code(self):
- """Test with state code."""
- with tempfile.TemporaryDirectory() as tempdirname:
- path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- }
- assert setup_component(
- self.hass,
- switch.DOMAIN,
- {
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
- }
- },
- )
- self.hass.block_till_done()
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
-
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
-
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
-
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
-
- def test_assumed_state_should_be_true_if_command_state_is_none(self):
- """Test with state value."""
- # args: hass, device_name, friendly_name, command_on, command_off,
- # command_state, value_template
- init_args = [
- self.hass,
- "test_device_name",
- "Test friendly name!",
- "echo 'on command'",
- "echo 'off command'",
- None,
- None,
- 15,
- ]
-
- no_state_device = command_line.CommandSwitch(*init_args)
- assert no_state_device.assumed_state
-
- # Set state command
- init_args[-3] = "cat {}"
-
- state_device = command_line.CommandSwitch(*init_args)
- assert not state_device.assumed_state
-
- def test_entity_id_set_correctly(self):
- """Test that entity_id is set correctly from object_id."""
- init_args = [
- self.hass,
- "test_device_name",
- "Test friendly name!",
- "echo 'on command'",
- "echo 'off command'",
- False,
- None,
- 15,
- ]
-
- test_switch = command_line.CommandSwitch(*init_args)
- assert test_switch.entity_id == "switch.test_device_name"
- assert test_switch.name == "Test friendly name!"
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+)
+from homeassistant.setup import async_setup_component
+
+
+async def test_state_none(hass):
+ """Test with none state."""
+ with tempfile.TemporaryDirectory() as tempdirname:
+ path = os.path.join(tempdirname, "switch_status")
+ test_switch = {
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
+ }
+ assert await async_setup_component(
+ hass,
+ switch.DOMAIN,
+ {
+ "switch": {
+ "platform": "command_line",
+ "switches": {"test": test_switch},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+
+async def test_state_value(hass):
+ """Test with state value."""
+ with tempfile.TemporaryDirectory() as tempdirname:
+ path = os.path.join(tempdirname, "switch_status")
+ test_switch = {
+ "command_state": f"cat {path}",
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
+ "value_template": '{{ value=="1" }}',
+ }
+ assert await async_setup_component(
+ hass,
+ switch.DOMAIN,
+ {
+ "switch": {
+ "platform": "command_line",
+ "switches": {"test": test_switch},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+
+async def test_state_json_value(hass):
+ """Test with state JSON value."""
+ with tempfile.TemporaryDirectory() as tempdirname:
+ path = os.path.join(tempdirname, "switch_status")
+ oncmd = json.dumps({"status": "ok"})
+ offcmd = json.dumps({"status": "nope"})
+ test_switch = {
+ "command_state": f"cat {path}",
+ "command_on": f"echo '{oncmd}' > {path}",
+ "command_off": f"echo '{offcmd}' > {path}",
+ "value_template": '{{ value_json.status=="ok" }}',
+ }
+ assert await async_setup_component(
+ hass,
+ switch.DOMAIN,
+ {
+ "switch": {
+ "platform": "command_line",
+ "switches": {"test": test_switch},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+
+async def test_state_code(hass):
+ """Test with state code."""
+ with tempfile.TemporaryDirectory() as tempdirname:
+ path = os.path.join(tempdirname, "switch_status")
+ test_switch = {
+ "command_state": f"cat {path}",
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
+ }
+ assert await async_setup_component(
+ hass,
+ switch.DOMAIN,
+ {
+ "switch": {
+ "platform": "command_line",
+ "switches": {"test": test_switch},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
+
+
+def test_assumed_state_should_be_true_if_command_state_is_none(hass):
+ """Test with state value."""
+ # args: hass, device_name, friendly_name, command_on, command_off,
+ # command_state, value_template
+ init_args = [
+ hass,
+ "test_device_name",
+ "Test friendly name!",
+ "echo 'on command'",
+ "echo 'off command'",
+ None,
+ None,
+ 15,
+ ]
+
+ no_state_device = command_line.CommandSwitch(*init_args)
+ assert no_state_device.assumed_state
+
+ # Set state command
+ init_args[-3] = "cat {}"
+
+ state_device = command_line.CommandSwitch(*init_args)
+ assert not state_device.assumed_state
+
+
+def test_entity_id_set_correctly(hass):
+ """Test that entity_id is set correctly from object_id."""
+ init_args = [
+ hass,
+ "test_device_name",
+ "Test friendly name!",
+ "echo 'on command'",
+ "echo 'off command'",
+ False,
+ None,
+ 15,
+ ]
+
+ test_switch = command_line.CommandSwitch(*init_args)
+ assert test_switch.entity_id == "switch.test_device_name"
+ assert test_switch.name == "Test friendly name!"
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index 2a696624e0c8e8..d63d10437cc0ec 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -4,6 +4,7 @@
import pytest
from homeassistant.components.config import entity_registry
+from homeassistant.const import ATTR_ICON
from homeassistant.helpers.entity_registry import RegistryEntry
from tests.common import MockEntity, MockEntityPlatform, mock_registry
@@ -140,7 +141,7 @@ async def test_update_entity(hass, client):
state = hass.states.get("test_domain.world")
assert state is not None
assert state.name == "before update"
- assert state.attributes["icon"] == "icon:before update"
+ assert state.attributes[ATTR_ICON] == "icon:before update"
# UPDATE NAME & ICON
await client.send_json(
@@ -171,7 +172,7 @@ async def test_update_entity(hass, client):
state = hass.states.get("test_domain.world")
assert state.name == "after update"
- assert state.attributes["icon"] == "icon:after update"
+ assert state.attributes[ATTR_ICON] == "icon:after update"
# UPDATE DISABLED_BY TO USER
await client.send_json(
diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py
index fc296a53ddfbd9..b758a37db1d16a 100644
--- a/tests/components/coolmaster/test_config_flow.py
+++ b/tests/components/coolmaster/test_config_flow.py
@@ -60,7 +60,7 @@ async def test_form_timeout(hass):
)
assert result2["type"] == "form"
- assert result2["errors"] == {"base": "connection_error"}
+ assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_connection_refused(hass):
@@ -78,7 +78,7 @@ async def test_form_connection_refused(hass):
)
assert result2["type"] == "form"
- assert result2["errors"] == {"base": "connection_error"}
+ assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_no_units(hass):
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 30f7251e067832..f3a5cede08eebc 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -7,6 +7,8 @@
DEVICE_CLASS_MOTION,
DEVICE_CLASS_VIBRATION,
)
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.helpers.entity_registry import async_entries_for_config_entry
from homeassistant.setup import async_setup_component
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -65,8 +67,7 @@ async def test_platform_manually_configured(hass):
async def test_no_binary_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -74,23 +75,15 @@ async def test_binary_sensors(hass):
"""Test successful creation of binary sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "binary_sensor.presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
- assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids
- assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 3
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ assert len(hass.states.async_all()) == 3
presence_sensor = hass.states.get("binary_sensor.presence_sensor")
assert presence_sensor.state == "off"
assert presence_sensor.attributes["device_class"] == DEVICE_CLASS_MOTION
-
- temperature_sensor = hass.states.get("binary_sensor.temperature_sensor")
- assert temperature_sensor is None
-
- clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor")
- assert clip_presence_sensor is None
-
+ assert hass.states.get("binary_sensor.temperature_sensor") is None
+ assert hass.states.get("binary_sensor.clip_presence_sensor") is None
vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
assert vibration_sensor.state == "on"
assert vibration_sensor.attributes["device_class"] == DEVICE_CLASS_VIBRATION
@@ -105,10 +98,9 @@ async def test_binary_sensors(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- presence_sensor = hass.states.get("binary_sensor.presence_sensor")
- assert presence_sensor.state == "on"
+ assert hass.states.get("binary_sensor.presence_sensor").state == "on"
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
@@ -117,56 +109,44 @@ async def test_allow_clip_sensor(hass):
"""Test that CLIP sensors can be allowed."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(
+ config_entry = await setup_deconz_integration(
hass,
options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
get_state_response=data,
)
- assert "binary_sensor.presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
- assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 4
-
- presence_sensor = hass.states.get("binary_sensor.presence_sensor")
- assert presence_sensor.state == "off"
- temperature_sensor = hass.states.get("binary_sensor.temperature_sensor")
- assert temperature_sensor is None
+ assert len(hass.states.async_all()) == 4
+ assert hass.states.get("binary_sensor.presence_sensor").state == "off"
+ assert hass.states.get("binary_sensor.temperature_sensor") is None
+ assert hass.states.get("binary_sensor.clip_presence_sensor").state == "off"
+ assert hass.states.get("binary_sensor.vibration_sensor").state == "on"
- clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor")
- assert clip_presence_sensor.state == "off"
-
- vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
- assert vibration_sensor.state == "on"
+ # Disallow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
)
await hass.async_block_till_done()
- assert "binary_sensor.presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
- assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids
- assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
assert len(hass.states.async_all()) == 3
+ assert hass.states.get("binary_sensor.clip_presence_sensor") is None
+
+ # Allow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
)
await hass.async_block_till_done()
- assert "binary_sensor.presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
- assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids
- assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
assert len(hass.states.async_all()) == 4
+ assert hass.states.get("binary_sensor.clip_presence_sensor").state == "off"
async def test_add_new_binary_sensor(hass):
"""Test that adding a new binary sensor works."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ config_entry = await setup_deconz_integration(hass)
+ gateway = deconz.gateway.get_gateway_from_config_entry(hass, config_entry)
+ assert len(hass.states.async_all()) == 0
state_added_event = {
"t": "event",
@@ -178,7 +158,32 @@ async def test_add_new_binary_sensor(hass):
gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
- assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("binary_sensor.presence_sensor").state == "off"
- presence_sensor = hass.states.get("binary_sensor.presence_sensor")
- assert presence_sensor.state == "off"
+
+async def test_add_new_binary_sensor_ignored(hass):
+ """Test that adding a new binary sensor is not allowed."""
+ config_entry = await setup_deconz_integration(
+ hass,
+ options={deconz.gateway.CONF_ALLOW_NEW_DEVICES: False},
+ )
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ assert len(hass.states.async_all()) == 0
+
+ state_added_event = {
+ "t": "event",
+ "e": "added",
+ "r": "sensors",
+ "id": "1",
+ "sensor": deepcopy(SENSORS["1"]),
+ }
+ gateway.api.event_handler(state_added_event)
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 0
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ assert (
+ len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0
+ )
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index ddc89295cbad76..3eb893d8a6e186 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -3,6 +3,7 @@
from homeassistant.components import deconz
import homeassistant.components.climate as climate
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.setup import async_setup_component
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -25,14 +26,6 @@
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Presence sensor id",
- "name": "Presence sensor",
- "type": "ZHAPresence",
- "state": {"presence": False},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
"id": "CLIP thermostat id",
"name": "CLIP thermostat",
"type": "CLIPThermostat",
@@ -56,8 +49,7 @@ async def test_platform_manually_configured(hass):
async def test_no_sensors(hass):
"""Test that no sensors in deconz results in no climate entities."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -65,28 +57,17 @@ async def test_climate_devices(hass):
"""Test successful creation of sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "climate.thermostat" in gateway.deconz_ids
- assert "sensor.thermostat" not in gateway.deconz_ids
- assert "sensor.thermostat_battery_level" in gateway.deconz_ids
- assert "climate.presence_sensor" not in gateway.deconz_ids
- assert "climate.clip_thermostat" not in gateway.deconz_ids
- assert len(hass.states.async_all()) == 3
-
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "auto"
-
- thermostat = hass.states.get("sensor.thermostat")
- assert thermostat is None
-
- thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level")
- assert thermostat_battery_level.state == "100"
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- presence_sensor = hass.states.get("climate.presence_sensor")
- assert presence_sensor is None
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("climate.thermostat").state == "auto"
+ assert hass.states.get("sensor.thermostat") is None
+ assert hass.states.get("sensor.thermostat_battery_level").state == "100"
+ assert hass.states.get("climate.presence_sensor") is None
+ assert hass.states.get("climate.clip_thermostat") is None
- clip_thermostat = hass.states.get("climate.clip_thermostat")
- assert clip_thermostat is None
+ # Event signals thermostat configured off
state_changed_event = {
"t": "event",
@@ -98,8 +79,9 @@ async def test_climate_devices(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "off"
+ assert hass.states.get("climate.thermostat").state == "off"
+
+ # Event signals thermostat state on
state_changed_event = {
"t": "event",
@@ -112,8 +94,9 @@ async def test_climate_devices(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "heat"
+ assert hass.states.get("climate.thermostat").state == "heat"
+
+ # Event signals thermostat state off
state_changed_event = {
"t": "event",
@@ -125,13 +108,14 @@ async def test_climate_devices(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "off"
+ assert hass.states.get("climate.thermostat").state == "off"
# Verify service calls
thermostat_device = gateway.api.sensors["1"]
+ # Service set HVAC mode to auto
+
with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
climate.DOMAIN,
@@ -144,6 +128,8 @@ async def test_climate_devices(hass):
"put", "/sensors/1/config", json={"mode": "auto"}
)
+ # Service set HVAC mode to heat
+
with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
climate.DOMAIN,
@@ -156,6 +142,8 @@ async def test_climate_devices(hass):
"put", "/sensors/1/config", json={"mode": "heat"}
)
+ # Service set HVAC mode to off
+
with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
climate.DOMAIN,
@@ -167,6 +155,8 @@ async def test_climate_devices(hass):
"put", "/sensors/1/config", json={"mode": "off"}
)
+ # Service set temperature to 20
+
with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
climate.DOMAIN,
@@ -178,7 +168,7 @@ async def test_climate_devices(hass):
"put", "/sensors/1/config", json={"heatsetpoint": 2000.0}
)
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
@@ -187,67 +177,47 @@ async def test_clip_climate_device(hass):
"""Test successful creation of sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(
+ config_entry = await setup_deconz_integration(
hass,
options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
get_state_response=data,
)
- assert "climate.thermostat" in gateway.deconz_ids
- assert "sensor.thermostat" not in gateway.deconz_ids
- assert "sensor.thermostat_battery_level" in gateway.deconz_ids
- assert "climate.presence_sensor" not in gateway.deconz_ids
- assert "climate.clip_thermostat" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 4
-
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "auto"
-
- thermostat = hass.states.get("sensor.thermostat")
- assert thermostat is None
- thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level")
- assert thermostat_battery_level.state == "100"
-
- presence_sensor = hass.states.get("climate.presence_sensor")
- assert presence_sensor is None
+ assert len(hass.states.async_all()) == 3
+ assert hass.states.get("climate.thermostat").state == "auto"
+ assert hass.states.get("sensor.thermostat") is None
+ assert hass.states.get("sensor.thermostat_battery_level").state == "100"
+ assert hass.states.get("climate.clip_thermostat").state == "heat"
- clip_thermostat = hass.states.get("climate.clip_thermostat")
- assert clip_thermostat.state == "heat"
+ # Disallow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
)
await hass.async_block_till_done()
- assert "climate.thermostat" in gateway.deconz_ids
- assert "sensor.thermostat" not in gateway.deconz_ids
- assert "sensor.thermostat_battery_level" in gateway.deconz_ids
- assert "climate.presence_sensor" not in gateway.deconz_ids
- assert "climate.clip_thermostat" not in gateway.deconz_ids
- assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("climate.clip_thermostat") is None
+
+ # Allow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
)
await hass.async_block_till_done()
- assert "climate.thermostat" in gateway.deconz_ids
- assert "sensor.thermostat" not in gateway.deconz_ids
- assert "sensor.thermostat_battery_level" in gateway.deconz_ids
- assert "climate.presence_sensor" not in gateway.deconz_ids
- assert "climate.clip_thermostat" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 4
+ assert len(hass.states.async_all()) == 3
+ assert hass.states.get("climate.clip_thermostat").state == "heat"
async def test_verify_state_update(hass):
"""Test that state update properly."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "climate.thermostat" in gateway.deconz_ids
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "auto"
+ assert hass.states.get("climate.thermostat").state == "auto"
state_changed_event = {
"t": "event",
@@ -259,15 +229,15 @@ async def test_verify_state_update(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "auto"
+ assert hass.states.get("climate.thermostat").state == "auto"
assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"}
async def test_add_new_climate_device(hass):
"""Test that adding a new climate device works."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ assert len(hass.states.async_all()) == 0
state_added_event = {
"t": "event",
@@ -279,7 +249,6 @@ async def test_add_new_climate_device(hass):
gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
- assert "climate.thermostat" in gateway.deconz_ids
-
- thermostat = hass.states.get("climate.thermostat")
- assert thermostat.state == "auto"
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("climate.thermostat").state == "auto"
+ assert hass.states.get("sensor.thermostat_battery_level").state == "100"
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index 43536a44bbece1..70894e8926a894 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -14,15 +14,18 @@
from homeassistant.components.deconz.const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
+ CONF_ALLOW_NEW_DEVICES,
CONF_MASTER_GATEWAY,
DOMAIN,
)
-from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration
from tests.async_mock import patch
+BAD_BRIDGEID = "0000000000000000"
+
async def test_flow_discovered_bridges(hass, aioclient_mock):
"""Test that config flow works for discovered bridges."""
@@ -32,7 +35,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock):
{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80},
{"id": "1234E567890A", "internalipaddress": "5.6.7.8", "internalport": 80},
],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -52,7 +55,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock):
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -73,7 +76,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock):
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -98,13 +101,13 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock):
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://1.2.3.4:80/api/{API_KEY}/config",
json={"bridgeid": BRIDGEID},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -125,7 +128,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock):
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -146,13 +149,13 @@ async def test_flow_manual_configuration(hass, aioclient_mock):
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://1.2.3.4:80/api/{API_KEY}/config",
json={"bridgeid": BRIDGEID},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -196,12 +199,12 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien
async def test_manual_configuration_update_configuration(hass, aioclient_mock):
"""Test that manual configuration can update existing config entry."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -222,13 +225,13 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock):
aioclient_mock.post(
"http://2.3.4.5:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://2.3.4.5:80/api/{API_KEY}/config",
json={"bridgeid": BRIDGEID},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -237,7 +240,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
+ assert config_entry.data[CONF_HOST] == "2.3.4.5"
async def test_manual_configuration_dont_update_configuration(hass, aioclient_mock):
@@ -247,7 +250,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -268,13 +271,13 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://1.2.3.4:80/api/{API_KEY}/config",
json={"bridgeid": BRIDGEID},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -290,7 +293,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock):
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -311,7 +314,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock):
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -331,7 +334,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock):
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -374,7 +377,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
aioclient_mock.post(
"http://1.2.3.4:80/api",
json=[{"success": {"username": API_KEY}}],
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -390,6 +393,35 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
}
+async def test_flow_ssdp_discovery_bad_bridge_id_aborts(hass, aioclient_mock):
+ """Test that config flow aborts if deCONZ signals no radio hardware available."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data={
+ ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/",
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_SERIAL: BAD_BRIDGEID,
+ },
+ context={"source": "ssdp"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "link"
+
+ aioclient_mock.post(
+ "http://1.2.3.4:80/api",
+ json=[{"success": {"username": API_KEY}}],
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "no_hardware_available"
+
+
async def test_ssdp_discovery_not_deconz_bridge(hass):
"""Test a non deconz bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
@@ -404,7 +436,7 @@ async def test_ssdp_discovery_not_deconz_bridge(hass):
async def test_ssdp_discovery_update_configuration(hass):
"""Test if a discovered bridge is configured but updates with new attributes."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
with patch(
"homeassistant.components.deconz.async_setup_entry",
@@ -423,13 +455,13 @@ async def test_ssdp_discovery_update_configuration(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
+ assert config_entry.data[CONF_HOST] == "2.3.4.5"
assert len(mock_setup_entry.mock_calls) == 1
async def test_ssdp_discovery_dont_update_configuration(hass):
"""Test if a discovered bridge has already been configured."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -443,12 +475,12 @@ async def test_ssdp_discovery_dont_update_configuration(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[CONF_HOST] == "1.2.3.4"
+ assert config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass):
"""Test to ensure the SSDP discovery does not update an Hass.io entry."""
- gateway = await setup_deconz_integration(hass, source="hassio")
+ config_entry = await setup_deconz_integration(hass, source="hassio")
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -462,7 +494,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[CONF_HOST] == "1.2.3.4"
+ assert config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_flow_hassio_discovery(hass):
@@ -505,7 +537,7 @@ async def test_flow_hassio_discovery(hass):
async def test_hassio_discovery_update_configuration(hass):
"""Test we can update an existing config entry."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
with patch(
"homeassistant.components.deconz.async_setup_entry",
@@ -525,9 +557,9 @@ async def test_hassio_discovery_update_configuration(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5"
- assert gateway.config_entry.data[CONF_PORT] == 8080
- assert gateway.config_entry.data[CONF_API_KEY] == "updated"
+ assert config_entry.data[CONF_HOST] == "2.3.4.5"
+ assert config_entry.data[CONF_PORT] == 8080
+ assert config_entry.data[CONF_API_KEY] == "updated"
assert len(mock_setup_entry.mock_calls) == 1
@@ -552,21 +584,26 @@ async def test_hassio_discovery_dont_update_configuration(hass):
async def test_option_flow(hass):
"""Test config flow options."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
- result = await hass.config_entries.options.async_init(gateway.config_entry.entry_id)
+ 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"] == "deconz_devices"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
- user_input={CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False},
+ user_input={
+ CONF_ALLOW_CLIP_SENSOR: False,
+ CONF_ALLOW_DECONZ_GROUPS: False,
+ CONF_ALLOW_NEW_DEVICES: False,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
CONF_ALLOW_CLIP_SENSOR: False,
CONF_ALLOW_DECONZ_GROUPS: False,
+ CONF_ALLOW_NEW_DEVICES: False,
CONF_MASTER_GATEWAY: True,
}
diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py
index 095ae7e4bc5223..799917b26fd142 100644
--- a/tests/components/deconz/test_cover.py
+++ b/tests/components/deconz/test_cover.py
@@ -3,6 +3,7 @@
from homeassistant.components import deconz
import homeassistant.components.cover as cover
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.setup import async_setup_component
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -65,8 +66,7 @@ async def test_platform_manually_configured(hass):
async def test_no_covers(hass):
"""Test that no cover entities are created."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -74,16 +74,17 @@ async def test_cover(hass):
"""Test that all supported cover entities are created."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["lights"] = deepcopy(COVERS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "cover.level_controllable_cover" in gateway.deconz_ids
- assert "cover.window_covering_device" in gateway.deconz_ids
- assert "cover.unsupported_cover" not in gateway.deconz_ids
- assert "cover.deconz_old_brightness_cover" in gateway.deconz_ids
- assert "cover.window_covering_controller" in gateway.deconz_ids
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
assert len(hass.states.async_all()) == 5
+ assert hass.states.get("cover.level_controllable_cover").state == "open"
+ assert hass.states.get("cover.window_covering_device").state == "closed"
+ assert hass.states.get("cover.unsupported_cover") is None
+ assert hass.states.get("cover.deconz_old_brightness_cover").state == "open"
+ assert hass.states.get("cover.window_covering_controller").state == "closed"
- level_controllable_cover = hass.states.get("cover.level_controllable_cover")
- assert level_controllable_cover.state == "open"
+ # Event signals cover is closed
state_changed_event = {
"t": "event",
@@ -95,11 +96,14 @@ async def test_cover(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- level_controllable_cover = hass.states.get("cover.level_controllable_cover")
- assert level_controllable_cover.state == "closed"
+ assert hass.states.get("cover.level_controllable_cover").state == "closed"
+
+ # Verify service calls
level_controllable_cover_device = gateway.api.lights["1"]
+ # Service open cover
+
with patch.object(
level_controllable_cover_device, "_request", return_value=True
) as set_callback:
@@ -112,6 +116,8 @@ async def test_cover(hass):
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
+ # Service close cover
+
with patch.object(
level_controllable_cover_device, "_request", return_value=True
) as set_callback:
@@ -126,6 +132,8 @@ async def test_cover(hass):
"put", "/lights/1/state", json={"on": True, "bri": 254}
)
+ # Service stop cover movement
+
with patch.object(
level_controllable_cover_device, "_request", return_value=True
) as set_callback:
@@ -139,8 +147,7 @@ async def test_cover(hass):
set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0})
# Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly.
- deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover")
- assert deconz_old_brightness_cover.state == "open"
+ assert hass.states.get("cover.deconz_old_brightness_cover").state == "open"
state_changed_event = {
"t": "event",
@@ -153,8 +160,9 @@ async def test_cover(hass):
await hass.async_block_till_done()
deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover")
+ assert deconz_old_brightness_cover.state == "closed"
assert deconz_old_brightness_cover.attributes["current_position"] == 0
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py
index fc8d4f9d1bad63..252fd8c3062672 100644
--- a/tests/components/deconz/test_deconz_event.py
+++ b/tests/components/deconz/test_deconz_event.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -40,6 +41,14 @@
"config": {"battery": 100},
"uniqueid": "00:00:00:00:00:00:00:04-00",
},
+ "5": {
+ "id": "ZHA remote 1 id",
+ "name": "ZHA remote 1",
+ "type": "ZHASwitch",
+ "state": {"angle": 0, "buttonevent": 1000, "xy": [0.0, 0.0]},
+ "config": {"group": "4,5,6", "reachable": True, "on": True},
+ "uniqueid": "00:00:00:00:00:00:00:05-00",
+ },
}
@@ -47,25 +56,15 @@ async def test_deconz_events(hass):
"""Test successful creation of deconz events."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "sensor.switch_1" not in gateway.deconz_ids
- assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
- assert "sensor.switch_2" not in gateway.deconz_ids
- assert "sensor.switch_2_battery_level" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 3
- assert len(gateway.events) == 4
-
- switch_1 = hass.states.get("sensor.switch_1")
- assert switch_1 is None
-
- switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
- assert switch_1_battery_level is None
-
- switch_2 = hass.states.get("sensor.switch_2")
- assert switch_2 is None
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
- assert switch_2_battery_level.state == "100"
+ assert len(hass.states.async_all()) == 3
+ assert len(gateway.events) == 5
+ assert hass.states.get("sensor.switch_1") is None
+ assert hass.states.get("sensor.switch_1_battery_level") is None
+ assert hass.states.get("sensor.switch_2") is None
+ assert hass.states.get("sensor.switch_2_battery_level").state == "100"
events = async_capture_events(hass, CONF_DECONZ_EVENT)
@@ -101,7 +100,21 @@ async def test_deconz_events(hass):
"gesture": 0,
}
- await gateway.async_reset()
+ gateway.api.sensors["5"].update(
+ {"state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]}}
+ )
+ await hass.async_block_till_done()
+
+ assert len(events) == 4
+ assert events[3].data == {
+ "id": "zha_remote_1",
+ "unique_id": "00:00:00:00:00:00:00:05",
+ "event": 6002,
+ "angle": 110,
+ "xy": [0.5982, 0.3897],
+ }
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
assert len(gateway.events) == 0
diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py
index 61962b00fe696a..5c59579fde9dc8 100644
--- a/tests/components/deconz/test_device_trigger.py
+++ b/tests/components/deconz/test_device_trigger.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components.deconz import device_trigger
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -34,7 +35,8 @@ async def test_get_triggers(hass):
"""Test triggers work."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
device_id = gateway.events[0].device_id
triggers = await async_get_device_automations(hass, "trigger", device_id)
@@ -97,7 +99,8 @@ async def test_helper_successful(hass):
"""Verify trigger helper."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
device_id = gateway.events[0].device_id
deconz_event = device_trigger._get_deconz_event_from_device_id(hass, device_id)
assert deconz_event == gateway.events[0]
diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py
index 15ff304459b2e8..11516b36eca9b2 100644
--- a/tests/components/deconz/test_gateway.py
+++ b/tests/components/deconz/test_gateway.py
@@ -6,6 +6,7 @@
from homeassistant import config_entries
from homeassistant.components import deconz, ssdp
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from tests.async_mock import Mock, patch
@@ -66,8 +67,7 @@ async def setup_deconz_integration(
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- bridgeid = get_state_response["config"]["bridgeid"]
- return hass.data[deconz.DOMAIN].get(bridgeid)
+ return config_entry
async def test_gateway_setup(hass):
@@ -76,23 +76,25 @@ async def test_gateway_setup(hass):
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
return_value=True,
) as forward_entry_setup:
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway.bridgeid == BRIDGEID
assert gateway.master is True
assert gateway.option_allow_clip_sensor is False
assert gateway.option_allow_deconz_groups is True
+ assert gateway.option_allow_new_devices is True
assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
- entry = gateway.config_entry
- assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor")
- assert forward_entry_setup.mock_calls[1][1] == (entry, "climate")
- assert forward_entry_setup.mock_calls[2][1] == (entry, "cover")
- assert forward_entry_setup.mock_calls[3][1] == (entry, "light")
- assert forward_entry_setup.mock_calls[4][1] == (entry, "scene")
- assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor")
- assert forward_entry_setup.mock_calls[6][1] == (entry, "switch")
+ assert forward_entry_setup.mock_calls[0][1] == (config_entry, "binary_sensor")
+ assert forward_entry_setup.mock_calls[1][1] == (config_entry, "climate")
+ assert forward_entry_setup.mock_calls[2][1] == (config_entry, "cover")
+ assert forward_entry_setup.mock_calls[3][1] == (config_entry, "light")
+ assert forward_entry_setup.mock_calls[4][1] == (config_entry, "lock")
+ assert forward_entry_setup.mock_calls[5][1] == (config_entry, "scene")
+ assert forward_entry_setup.mock_calls[6][1] == (config_entry, "sensor")
+ assert forward_entry_setup.mock_calls[7][1] == (config_entry, "switch")
async def test_gateway_retry(hass):
@@ -110,13 +112,15 @@ async def test_gateway_setup_fails(hass):
with patch(
"homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception
):
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway is None
async def test_connection_status_signalling(hass):
"""Make sure that connection status triggers a dispatcher send."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
event_call = Mock()
unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call)
@@ -132,7 +136,8 @@ async def test_connection_status_signalling(hass):
async def test_update_address(hass):
"""Make sure that connection status triggers a dispatcher send."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway.api.host == "1.2.3.4"
with patch(
@@ -157,7 +162,8 @@ async def test_update_address(hass):
async def test_reset_after_successful_setup(hass):
"""Make sure that connection status triggers a dispatcher send."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
result = await gateway.async_reset()
await hass.async_block_till_done()
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
index 8d9d387c91ca6a..8fd29ec2368bfe 100644
--- a/tests/components/deconz/test_init.py
+++ b/tests/components/deconz/test_init.py
@@ -3,6 +3,7 @@
from copy import deepcopy
from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
@@ -47,7 +48,8 @@ async def test_setup_entry_no_available_bridge(hass):
async def test_setup_entry_successful(hass):
"""Test setup entry is successful."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
assert hass.data[deconz.DOMAIN]
assert gateway.bridgeid in hass.data[deconz.DOMAIN]
@@ -56,13 +58,15 @@ async def test_setup_entry_successful(hass):
async def test_setup_entry_multiple_gateways(hass):
"""Test setup entry is successful with multiple gateways."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
data = deepcopy(DECONZ_WEB_REQUEST)
data["config"]["bridgeid"] = "01234E56789B"
- gateway2 = await setup_deconz_integration(
+ config_entry2 = await setup_deconz_integration(
hass, get_state_response=data, entry_id="2"
)
+ gateway2 = get_gateway_from_config_entry(hass, config_entry2)
assert len(hass.data[deconz.DOMAIN]) == 2
assert hass.data[deconz.DOMAIN][gateway.bridgeid].master
@@ -71,26 +75,27 @@ async def test_setup_entry_multiple_gateways(hass):
async def test_unload_entry(hass):
"""Test being able to unload an entry."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
assert hass.data[deconz.DOMAIN]
- assert await deconz.async_unload_entry(hass, gateway.config_entry)
+ assert await deconz.async_unload_entry(hass, config_entry)
assert not hass.data[deconz.DOMAIN]
async def test_unload_entry_multiple_gateways(hass):
"""Test being able to unload an entry and master gateway gets moved."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
data = deepcopy(DECONZ_WEB_REQUEST)
data["config"]["bridgeid"] = "01234E56789B"
- gateway2 = await setup_deconz_integration(
+ config_entry2 = await setup_deconz_integration(
hass, get_state_response=data, entry_id="2"
)
+ gateway2 = get_gateway_from_config_entry(hass, config_entry2)
assert len(hass.data[deconz.DOMAIN]) == 2
- assert await deconz.async_unload_entry(hass, gateway.config_entry)
+ assert await deconz.async_unload_entry(hass, config_entry)
assert len(hass.data[deconz.DOMAIN]) == 1
assert hass.data[deconz.DOMAIN][gateway2.bridgeid].master
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index d070bd5b420525..00a3f49477e26a 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
import homeassistant.components.light as light
from homeassistant.setup import async_setup_component
@@ -67,6 +68,15 @@
"type": "On and Off light",
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
+ "5": {
+ "ctmax": 1000,
+ "ctmin": 0,
+ "id": "Tunable white light with bad maxmin values id",
+ "name": "Tunable white light with bad maxmin values",
+ "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
}
@@ -83,8 +93,7 @@ async def test_platform_manually_configured(hass):
async def test_no_lights_or_groups(hass):
"""Test that no lights or groups entities are created."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -93,15 +102,10 @@ async def test_lights_and_groups(hass):
data = deepcopy(DECONZ_WEB_REQUEST)
data["groups"] = deepcopy(GROUPS)
data["lights"] = deepcopy(LIGHTS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "light.rgb_light" in gateway.deconz_ids
- assert "light.tunable_white_light" in gateway.deconz_ids
- assert "light.light_group" in gateway.deconz_ids
- assert "light.empty_group" not in gateway.deconz_ids
- assert "light.on_off_switch" not in gateway.deconz_ids
- assert "light.on_off_light" in gateway.deconz_ids
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_all()) == 6
rgb_light = hass.states.get("light.rgb_light")
assert rgb_light.state == "on"
@@ -117,6 +121,15 @@ async def test_lights_and_groups(hass):
assert tunable_white_light.attributes["min_mireds"] == 155
assert tunable_white_light.attributes["supported_features"] == 2
+ tunable_white_light_bad_maxmin = hass.states.get(
+ "light.tunable_white_light_with_bad_maxmin_values"
+ )
+ assert tunable_white_light_bad_maxmin.state == "on"
+ assert tunable_white_light_bad_maxmin.attributes["color_temp"] == 2500
+ assert tunable_white_light_bad_maxmin.attributes["max_mireds"] == 650
+ assert tunable_white_light_bad_maxmin.attributes["min_mireds"] == 140
+ assert tunable_white_light_bad_maxmin.attributes["supported_features"] == 2
+
on_off_light = hass.states.get("light.on_off_light")
assert on_off_light.state == "on"
assert on_off_light.attributes["supported_features"] == 0
@@ -141,8 +154,12 @@ async def test_lights_and_groups(hass):
rgb_light = hass.states.get("light.rgb_light")
assert rgb_light.state == "off"
+ # Verify service calls
+
rgb_light_device = gateway.api.lights["1"]
+ # Service turn on light with short color loop
+
with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
light.DOMAIN,
@@ -170,6 +187,8 @@ async def test_lights_and_groups(hass):
},
)
+ # Service turn on light disabling color loop with long flashing
+
with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
light.DOMAIN,
@@ -189,6 +208,8 @@ async def test_lights_and_groups(hass):
json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"},
)
+ # Service turn on light with short flashing
+
with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
light.DOMAIN,
@@ -209,6 +230,8 @@ async def test_lights_and_groups(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
+ # Service turn off light with short flashing
+
with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
light.DOMAIN,
@@ -223,6 +246,8 @@ async def test_lights_and_groups(hass):
json={"bri": 0, "transitiontime": 50, "alert": "select"},
)
+ # Service turn off light with long flashing
+
with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
await hass.services.async_call(
light.DOMAIN,
@@ -235,63 +260,40 @@ async def test_lights_and_groups(hass):
"put", "/lights/1/state", json={"alert": "lselect"}
)
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
async def test_disable_light_groups(hass):
- """Test successful creation of sensor entities."""
+ """Test disallowing light groups work."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["groups"] = deepcopy(GROUPS)
data["lights"] = deepcopy(LIGHTS)
- gateway = await setup_deconz_integration(
+ config_entry = await setup_deconz_integration(
hass,
options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False},
get_state_response=data,
)
- assert "light.rgb_light" in gateway.deconz_ids
- assert "light.tunable_white_light" in gateway.deconz_ids
- assert "light.light_group" not in gateway.deconz_ids
- assert "light.empty_group" not in gateway.deconz_ids
- assert "light.on_off_switch" not in gateway.deconz_ids
- # 3 entities
- assert len(hass.states.async_all()) == 4
- rgb_light = hass.states.get("light.rgb_light")
- assert rgb_light is not None
-
- tunable_white_light = hass.states.get("light.tunable_white_light")
- assert tunable_white_light is not None
-
- light_group = hass.states.get("light.light_group")
- assert light_group is None
-
- empty_group = hass.states.get("light.empty_group")
- assert empty_group is None
+ assert len(hass.states.async_all()) == 5
+ assert hass.states.get("light.rgb_light")
+ assert hass.states.get("light.tunable_white_light")
+ assert hass.states.get("light.light_group") is None
+ assert hass.states.get("light.empty_group") is None
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: True}
+ config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: True}
)
await hass.async_block_till_done()
- assert "light.rgb_light" in gateway.deconz_ids
- assert "light.tunable_white_light" in gateway.deconz_ids
- assert "light.light_group" in gateway.deconz_ids
- assert "light.empty_group" not in gateway.deconz_ids
- assert "light.on_off_switch" not in gateway.deconz_ids
- # 3 entities
- assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_all()) == 6
+ assert hass.states.get("light.light_group")
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False}
+ config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False}
)
await hass.async_block_till_done()
- assert "light.rgb_light" in gateway.deconz_ids
- assert "light.tunable_white_light" in gateway.deconz_ids
- assert "light.light_group" not in gateway.deconz_ids
- assert "light.empty_group" not in gateway.deconz_ids
- assert "light.on_off_switch" not in gateway.deconz_ids
- # 3 entities
- assert len(hass.states.async_all()) == 4
+ assert len(hass.states.async_all()) == 5
+ assert hass.states.get("light.light_group") is None
diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py
new file mode 100644
index 00000000000000..cb7267aaadadcd
--- /dev/null
+++ b/tests/components/deconz/test_lock.py
@@ -0,0 +1,103 @@
+"""deCONZ lock platform tests."""
+from copy import deepcopy
+
+from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+import homeassistant.components.lock as lock
+from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.setup import async_setup_component
+
+from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
+
+from tests.async_mock import patch
+
+LOCKS = {
+ "1": {
+ "etag": "5c2ec06cde4bd654aef3a555fcd8ad12",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2020-08-22T15:29:03Z",
+ "manufacturername": "Danalock",
+ "modelid": "V3-BTZB",
+ "name": "Door lock",
+ "state": {"alert": "none", "on": False, "reachable": True},
+ "swversion": "19042019",
+ "type": "Door Lock",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+}
+
+
+async def test_platform_manually_configured(hass):
+ """Test that we do not discover anything or try to set up a gateway."""
+ assert (
+ await async_setup_component(
+ hass, lock.DOMAIN, {"lock": {"platform": deconz.DOMAIN}}
+ )
+ is True
+ )
+ assert deconz.DOMAIN not in hass.data
+
+
+async def test_no_locks(hass):
+ """Test that no lock entities are created."""
+ await setup_deconz_integration(hass)
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_locks(hass):
+ """Test that all supported lock entities are created."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = deepcopy(LOCKS)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED
+
+ door_lock = hass.states.get("lock.door_lock")
+ assert door_lock.state == STATE_UNLOCKED
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"on": True},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("lock.door_lock").state == STATE_LOCKED
+
+ # Verify service calls
+
+ door_lock_device = gateway.api.lights["1"]
+
+ # Service lock door
+
+ with patch.object(door_lock_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ lock.DOMAIN,
+ lock.SERVICE_LOCK,
+ {"entity_id": "lock.door_lock"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+
+ # Service unlock door
+
+ with patch.object(door_lock_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ lock.DOMAIN,
+ lock.SERVICE_UNLOCK,
+ {"entity_id": "lock.door_lock"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+
+ assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py
index 538c849e831dce..4717ccb0ba857a 100644
--- a/tests/components/deconz/test_scene.py
+++ b/tests/components/deconz/test_scene.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
import homeassistant.components.scene as scene
from homeassistant.setup import async_setup_component
@@ -35,8 +36,7 @@ async def test_platform_manually_configured(hass):
async def test_no_scenes(hass):
"""Test that scenes can be loaded without scenes being available."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -44,16 +44,18 @@ async def test_scenes(hass):
"""Test that scenes works."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["groups"] = deepcopy(GROUPS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- assert "scene.light_group_scene" in gateway.deconz_ids
assert len(hass.states.async_all()) == 1
+ assert hass.states.get("scene.light_group_scene")
- light_group_scene = hass.states.get("scene.light_group_scene")
- assert light_group_scene
+ # Verify service calls
group_scene = gateway.api.groups["1"].scenes["1"]
+ # Service turn on scene
+
with patch.object(group_scene, "_request", return_value=True) as set_callback:
await hass.services.async_call(
"scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True
@@ -61,6 +63,6 @@ async def test_scenes(hass):
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/groups/1/scenes/1/recall", json={})
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index 9d87c7b91cbc8f..5cfb01ddf9d526 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
import homeassistant.components.sensor as sensor
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
@@ -93,8 +94,7 @@ async def test_platform_manually_configured(hass):
async def test_no_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
@@ -102,41 +102,25 @@ async def test_sensors(hass):
"""Test successful creation of sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "sensor.light_level_sensor" in gateway.deconz_ids
- assert "sensor.presence_sensor" not in gateway.deconz_ids
- assert "sensor.switch_1" not in gateway.deconz_ids
- assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
- assert "sensor.switch_2" not in gateway.deconz_ids
- assert "sensor.switch_2_battery_level" in gateway.deconz_ids
- assert "sensor.daylight_sensor" not in gateway.deconz_ids
- assert "sensor.power_sensor" in gateway.deconz_ids
- assert "sensor.consumption_sensor" in gateway.deconz_ids
- assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
assert len(hass.states.async_all()) == 5
light_level_sensor = hass.states.get("sensor.light_level_sensor")
assert light_level_sensor.state == "999.8"
assert light_level_sensor.attributes["device_class"] == DEVICE_CLASS_ILLUMINANCE
- presence_sensor = hass.states.get("sensor.presence_sensor")
- assert presence_sensor is None
-
- switch_1 = hass.states.get("sensor.switch_1")
- assert switch_1 is None
-
- switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
- assert switch_1_battery_level is None
-
- switch_2 = hass.states.get("sensor.switch_2")
- assert switch_2 is None
+ assert hass.states.get("sensor.presence_sensor") is None
+ assert hass.states.get("sensor.switch_1") is None
+ assert hass.states.get("sensor.switch_1_battery_level") is None
+ assert hass.states.get("sensor.switch_2") is None
switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
assert switch_2_battery_level.state == "100"
assert switch_2_battery_level.attributes["device_class"] == DEVICE_CLASS_BATTERY
- daylight_sensor = hass.states.get("sensor.daylight_sensor")
- assert daylight_sensor is None
+ assert hass.states.get("sensor.daylight_sensor") is None
power_sensor = hass.states.get("sensor.power_sensor")
assert power_sensor.state == "6"
@@ -146,6 +130,10 @@ async def test_sensors(hass):
assert consumption_sensor.state == "0.002"
assert "device_class" not in consumption_sensor.attributes
+ assert hass.states.get("sensor.clip_light_level_sensor") is None
+
+ # Event signals new light level
+
state_changed_event = {
"t": "event",
"e": "changed",
@@ -155,6 +143,10 @@ async def test_sensors(hass):
}
gateway.api.event_handler(state_changed_event)
+ assert hass.states.get("sensor.light_level_sensor").state == "1.6"
+
+ # Event signals new battery level
+
state_changed_event = {
"t": "event",
"e": "changed",
@@ -165,13 +157,9 @@ async def test_sensors(hass):
gateway.api.event_handler(state_changed_event)
await hass.async_block_till_done()
- light_level_sensor = hass.states.get("sensor.light_level_sensor")
- assert light_level_sensor.state == "1.6"
-
- switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
- assert switch_2_battery_level.state == "75"
+ assert hass.states.get("sensor.switch_2_battery_level").state == "75"
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
@@ -180,92 +168,41 @@ async def test_allow_clip_sensors(hass):
"""Test that CLIP sensors can be allowed."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
- gateway = await setup_deconz_integration(
+ config_entry = await setup_deconz_integration(
hass,
options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
get_state_response=data,
)
- assert "sensor.light_level_sensor" in gateway.deconz_ids
- assert "sensor.presence_sensor" not in gateway.deconz_ids
- assert "sensor.switch_1" not in gateway.deconz_ids
- assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
- assert "sensor.switch_2" not in gateway.deconz_ids
- assert "sensor.switch_2_battery_level" in gateway.deconz_ids
- assert "sensor.daylight_sensor" not in gateway.deconz_ids
- assert "sensor.power_sensor" in gateway.deconz_ids
- assert "sensor.consumption_sensor" in gateway.deconz_ids
- assert "sensor.clip_light_level_sensor" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 6
-
- light_level_sensor = hass.states.get("sensor.light_level_sensor")
- assert light_level_sensor.state == "999.8"
-
- presence_sensor = hass.states.get("sensor.presence_sensor")
- assert presence_sensor is None
-
- switch_1 = hass.states.get("sensor.switch_1")
- assert switch_1 is None
-
- switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
- assert switch_1_battery_level is None
- switch_2 = hass.states.get("sensor.switch_2")
- assert switch_2 is None
-
- switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
- assert switch_2_battery_level.state == "100"
-
- daylight_sensor = hass.states.get("sensor.daylight_sensor")
- assert daylight_sensor is None
-
- power_sensor = hass.states.get("sensor.power_sensor")
- assert power_sensor.state == "6"
-
- consumption_sensor = hass.states.get("sensor.consumption_sensor")
- assert consumption_sensor.state == "0.002"
+ assert len(hass.states.async_all()) == 6
+ assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8"
- clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor")
- assert clip_light_level_sensor.state == "999.8"
+ # Disallow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False}
)
await hass.async_block_till_done()
- assert "sensor.light_level_sensor" in gateway.deconz_ids
- assert "sensor.presence_sensor" not in gateway.deconz_ids
- assert "sensor.switch_1" not in gateway.deconz_ids
- assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
- assert "sensor.switch_2" not in gateway.deconz_ids
- assert "sensor.switch_2_battery_level" in gateway.deconz_ids
- assert "sensor.daylight_sensor" not in gateway.deconz_ids
- assert "sensor.power_sensor" in gateway.deconz_ids
- assert "sensor.consumption_sensor" in gateway.deconz_ids
- assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids
assert len(hass.states.async_all()) == 5
+ assert hass.states.get("sensor.clip_light_level_sensor") is None
+
+ # Allow clip sensors
hass.config_entries.async_update_entry(
- gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
+ config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}
)
await hass.async_block_till_done()
- assert "sensor.light_level_sensor" in gateway.deconz_ids
- assert "sensor.presence_sensor" not in gateway.deconz_ids
- assert "sensor.switch_1" not in gateway.deconz_ids
- assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
- assert "sensor.switch_2" not in gateway.deconz_ids
- assert "sensor.switch_2_battery_level" in gateway.deconz_ids
- assert "sensor.daylight_sensor" not in gateway.deconz_ids
- assert "sensor.power_sensor" in gateway.deconz_ids
- assert "sensor.consumption_sensor" in gateway.deconz_ids
- assert "sensor.clip_light_level_sensor" in gateway.deconz_ids
assert len(hass.states.async_all()) == 6
+ assert hass.states.get("sensor.clip_light_level_sensor")
async def test_add_new_sensor(hass):
"""Test that adding a new sensor works."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ assert len(hass.states.async_all()) == 0
state_added_event = {
"t": "event",
@@ -277,28 +214,27 @@ async def test_add_new_sensor(hass):
gateway.api.event_handler(state_added_event)
await hass.async_block_till_done()
- assert "sensor.light_level_sensor" in gateway.deconz_ids
-
- light_level_sensor = hass.states.get("sensor.light_level_sensor")
- assert light_level_sensor.state == "999.8"
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("sensor.light_level_sensor").state == "999.8"
async def test_add_battery_later(hass):
"""Test that a sensor without an initial battery state creates a battery sensor once state exist."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = {"1": deepcopy(SENSORS["3"])}
- gateway = await setup_deconz_integration(hass, get_state_response=data)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
remote = gateway.api.sensors["1"]
- assert len(gateway.deconz_ids) == 0
+
+ assert len(hass.states.async_all()) == 0
assert len(gateway.events) == 1
- assert len(remote._callbacks) == 2
+ assert len(remote._callbacks) == 2 # Event and battery tracker
remote.update({"config": {"battery": 50}})
await hass.async_block_till_done()
- assert len(gateway.deconz_ids) == 1
+ assert len(hass.states.async_all()) == 1
assert len(gateway.events) == 1
- assert len(remote._callbacks) == 2
+ assert len(remote._callbacks) == 2 # Event and battery entity
- battery_sensor = hass.states.get("sensor.switch_1_battery_level")
- assert battery_sensor is not None
+ assert hass.states.get("sensor.switch_1_battery_level")
diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py
index e880ea1000ba83..e8cd1c23dc9d0d 100644
--- a/tests/components/deconz/test_services.py
+++ b/tests/components/deconz/test_services.py
@@ -1,12 +1,17 @@
"""deCONZ service tests."""
+from copy import deepcopy
+
import pytest
import voluptuous as vol
from homeassistant.components import deconz
from homeassistant.components.deconz.const import CONF_BRIDGE_ID
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.helpers.entity_registry import async_entries_for_config_entry
-from .test_gateway import BRIDGEID, setup_deconz_integration
+from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration
from tests.async_mock import Mock, patch
@@ -43,6 +48,17 @@
}
}
+SWITCH = {
+ "1": {
+ "id": "Switch 1 id",
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000, "gesture": 1},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+}
+
async def test_service_setup(hass):
"""Verify service setup works."""
@@ -52,7 +68,7 @@ async def test_service_setup(hass):
) as async_register:
await deconz.services.async_setup_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is True
- assert async_register.call_count == 2
+ assert async_register.call_count == 3
async def test_service_setup_already_registered(hass):
@@ -73,7 +89,7 @@ async def test_service_unload(hass):
) as async_remove:
await deconz.services.async_unload_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is False
- assert async_remove.call_count == 2
+ assert async_remove.call_count == 3
async def test_service_unload_not_registered(hass):
@@ -108,7 +124,8 @@ async def test_configure_service_with_field(hass):
async def test_configure_service_with_entity(hass):
"""Test that service invokes pydeconz with the correct path and data."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.deconz_ids["light.test"] = "/light/1"
data = {
@@ -128,7 +145,8 @@ async def test_configure_service_with_entity(hass):
async def test_configure_service_with_entity_and_field(hass):
"""Test that service invokes pydeconz with the correct path and data."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.deconz_ids["light.test"] = "/light/1"
data = {
@@ -179,7 +197,8 @@ async def test_configure_service_with_faulty_entity(hass):
async def test_service_refresh_devices(hass):
"""Test that service can refresh devices."""
- gateway = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
data = {CONF_BRIDGE_ID: BRIDGEID}
@@ -198,3 +217,67 @@ async def test_service_refresh_devices(hass):
"scene.group_1_name_scene_1": "/groups/1/scenes/1",
"sensor.sensor_1_name": "/sensors/1",
}
+
+
+async def test_remove_orphaned_entries_service(hass):
+ """Test service works and also don't remove more than expected."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = deepcopy(LIGHT)
+ data["sensors"] = deepcopy(SWITCH)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+
+ data = {CONF_BRIDGE_ID: BRIDGEID}
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id, identifiers={("mac", "123")}
+ )
+
+ assert (
+ len(
+ [
+ entry
+ for entry in device_registry.devices.values()
+ if config_entry.entry_id in entry.config_entries
+ ]
+ )
+ == 4 # Gateway, light, switch and orphan
+ )
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ deconz.DOMAIN,
+ "12345",
+ suggested_object_id="Orphaned sensor",
+ config_entry=config_entry,
+ device_id=device.id,
+ )
+
+ assert (
+ len(async_entries_for_config_entry(entity_registry, config_entry.entry_id))
+ == 3 # Light, switch battery and orphan
+ )
+
+ await hass.services.async_call(
+ deconz.DOMAIN,
+ deconz.services.SERVICE_REMOVE_ORPHANED_ENTRIES,
+ service_data=data,
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ len(
+ [
+ entry
+ for entry in device_registry.devices.values()
+ if config_entry.entry_id in entry.config_entries
+ ]
+ )
+ == 3 # Gateway, light and switch
+ )
+
+ assert (
+ len(async_entries_for_config_entry(entity_registry, config_entry.entry_id))
+ == 2 # Light and switch battery
+ )
diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py
index b441868859beb8..e4d5dcea5903f2 100644
--- a/tests/components/deconz/test_switch.py
+++ b/tests/components/deconz/test_switch.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from homeassistant.components import deconz
+from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
import homeassistant.components.switch as switch
from homeassistant.setup import async_setup_component
@@ -9,7 +10,7 @@
from tests.async_mock import patch
-SWITCHES = {
+POWER_PLUGS = {
"1": {
"id": "On off switch id",
"name": "On off switch",
@@ -25,20 +26,13 @@
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
"3": {
- "id": "Warning device id",
- "name": "Warning device",
- "type": "Warning device",
- "state": {"alert": "lselect", "reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "4": {
"id": "Unsupported switch id",
"name": "Unsupported switch",
- "type": "Not a smart plug",
+ "type": "Not a switch",
"state": {"reachable": True},
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
- "5": {
+ "4": {
"id": "On off relay id",
"name": "On off relay",
"state": {"on": True, "reachable": True},
@@ -47,6 +41,23 @@
},
}
+SIRENS = {
+ "1": {
+ "id": "Warning device id",
+ "name": "Warning device",
+ "type": "Warning device",
+ "state": {"alert": "lselect", "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "id": "Unsupported switch id",
+ "name": "Unsupported switch",
+ "type": "Not a switch",
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+}
+
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
@@ -61,34 +72,22 @@ async def test_platform_manually_configured(hass):
async def test_no_switches(hass):
"""Test that no switch entities are created."""
- gateway = await setup_deconz_integration(hass)
- assert len(gateway.deconz_ids) == 0
+ await setup_deconz_integration(hass)
assert len(hass.states.async_all()) == 0
-async def test_switches(hass):
+async def test_power_plugs(hass):
"""Test that all supported switch entities are created."""
data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(SWITCHES)
- gateway = await setup_deconz_integration(hass, get_state_response=data)
- assert "switch.on_off_switch" in gateway.deconz_ids
- assert "switch.smart_plug" in gateway.deconz_ids
- assert "switch.warning_device" in gateway.deconz_ids
- assert "switch.unsupported_switch" not in gateway.deconz_ids
- assert "switch.on_off_relay" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 5
-
- on_off_switch = hass.states.get("switch.on_off_switch")
- assert on_off_switch.state == "on"
-
- smart_plug = hass.states.get("switch.smart_plug")
- assert smart_plug.state == "off"
+ data["lights"] = deepcopy(POWER_PLUGS)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
- warning_device = hass.states.get("switch.warning_device")
- assert warning_device.state == "on"
-
- on_off_relay = hass.states.get("switch.on_off_relay")
- assert on_off_relay.state == "on"
+ assert len(hass.states.async_all()) == 4
+ assert hass.states.get("switch.on_off_switch").state == "on"
+ assert hass.states.get("switch.smart_plug").state == "off"
+ assert hass.states.get("switch.on_off_relay").state == "on"
+ assert hass.states.get("switch.unsupported_switch") is None
state_changed_event = {
"t": "event",
@@ -98,24 +97,15 @@ async def test_switches(hass):
"state": {"on": False},
}
gateway.api.event_handler(state_changed_event)
- state_changed_event = {
- "t": "event",
- "e": "changed",
- "r": "lights",
- "id": "3",
- "state": {"alert": None},
- }
- gateway.api.event_handler(state_changed_event)
- await hass.async_block_till_done()
- on_off_switch = hass.states.get("switch.on_off_switch")
- assert on_off_switch.state == "off"
+ assert hass.states.get("switch.on_off_switch").state == "off"
- warning_device = hass.states.get("switch.warning_device")
- assert warning_device.state == "off"
+ # Verify service calls
on_off_switch_device = gateway.api.lights["1"]
+ # Service turn on power plug
+
with patch.object(
on_off_switch_device, "_request", return_value=True
) as set_callback:
@@ -128,6 +118,8 @@ async def test_switches(hass):
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+ # Service turn off power plug
+
with patch.object(
on_off_switch_device, "_request", return_value=True
) as set_callback:
@@ -140,7 +132,38 @@ async def test_switches(hass):
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
- warning_device_device = gateway.api.lights["3"]
+ await hass.config_entries.async_unload(config_entry.entry_id)
+
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_sirens(hass):
+ """Test that siren entities are created."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = deepcopy(SIRENS)
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("switch.warning_device").state == "on"
+ assert hass.states.get("switch.unsupported_switch") is None
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"alert": None},
+ }
+ gateway.api.event_handler(state_changed_event)
+
+ assert hass.states.get("switch.warning_device").state == "off"
+
+ # Verify service calls
+
+ warning_device_device = gateway.api.lights["1"]
+
+ # Service turn on siren
with patch.object(
warning_device_device, "_request", return_value=True
@@ -153,9 +176,11 @@ async def test_switches(hass):
)
await hass.async_block_till_done()
set_callback.assert_called_with(
- "put", "/lights/3/state", json={"alert": "lselect"}
+ "put", "/lights/1/state", json={"alert": "lselect"}
)
+ # Service turn off siren
+
with patch.object(
warning_device_device, "_request", return_value=True
) as set_callback:
@@ -167,9 +192,9 @@ async def test_switches(hass):
)
await hass.async_block_till_done()
set_callback.assert_called_with(
- "put", "/lights/3/state", json={"alert": "none"}
+ "put", "/lights/1/state", json={"alert": "none"}
)
- await gateway.async_reset()
+ await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index 7edf1dc7d60a9a..6bf9cc44c56620 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -6,13 +6,6 @@
from tests.async_mock import patch
-@pytest.fixture(autouse=True)
-def mock_zeroconf():
- """Mock zeroconf."""
- with patch("homeassistant.components.zeroconf.HaZeroconf"):
- yield
-
-
@pytest.fixture(autouse=True)
def mock_ssdp():
"""Mock ssdp."""
@@ -34,6 +27,6 @@ def recorder_url_mock():
yield
-async def test_setup(hass):
+async def test_setup(hass, mock_zeroconf):
"""Test setup."""
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py
index 91e14247834d4c..aa6ff39cb0edf5 100644
--- a/tests/components/demo/test_climate.py
+++ b/tests/components/demo/test_climate.py
@@ -10,6 +10,7 @@
ATTR_FAN_MODE,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
+ ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_HUMIDITY,
ATTR_MAX_TEMP,
@@ -26,13 +27,25 @@
HVAC_MODE_OFF,
PRESET_AWAY,
PRESET_ECO,
+ SERVICE_SET_AUX_HEAT,
+ SERVICE_SET_FAN_MODE,
+ SERVICE_SET_HUMIDITY,
+ SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_PRESET_MODE,
+ SERVICE_SET_SWING_MODE,
+ SERVICE_SET_TEMPERATURE,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_TEMPERATURE,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
)
-from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM
-from tests.components.climate import common
-
ENTITY_CLIMATE = "climate.hvac"
ENTITY_ECOBEE = "climate.ecobee"
ENTITY_HEATPUMP = "climate.heatpump"
@@ -82,9 +95,14 @@ async def test_set_only_target_temp_bad_attr(hass):
assert state.attributes.get(ATTR_TEMPERATURE) == 21
with pytest.raises(vol.Invalid):
- await common.async_set_temperature(hass, None, ENTITY_CLIMATE)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_TEMPERATURE: None},
+ blocking=True,
+ )
- await hass.async_block_till_done()
+ state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_TEMPERATURE) == 21
@@ -93,8 +111,12 @@ async def test_set_only_target_temp(hass):
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_TEMPERATURE) == 21
- await common.async_set_temperature(hass, 30, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_TEMPERATURE: 30},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_TEMPERATURE) == 30.0
@@ -105,8 +127,12 @@ async def test_set_only_target_temp_with_convert(hass):
state = hass.states.get(ENTITY_HEATPUMP)
assert state.attributes.get(ATTR_TEMPERATURE) == 20
- await common.async_set_temperature(hass, 21, ENTITY_HEATPUMP)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: ENTITY_HEATPUMP, ATTR_TEMPERATURE: 21},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_HEATPUMP)
assert state.attributes.get(ATTR_TEMPERATURE) == 21.0
@@ -119,10 +145,16 @@ async def test_set_target_temp_range(hass):
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 21.0
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24.0
- await common.async_set_temperature(
- hass, target_temp_high=25, target_temp_low=20, entity_id=ENTITY_ECOBEE
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ ATTR_ENTITY_ID: ENTITY_ECOBEE,
+ ATTR_TARGET_TEMP_LOW: 20,
+ ATTR_TARGET_TEMP_HIGH: 25,
+ },
+ blocking=True,
)
- await hass.async_block_till_done()
state = hass.states.get(ENTITY_ECOBEE)
assert state.attributes.get(ATTR_TEMPERATURE) is None
@@ -138,14 +170,16 @@ async def test_set_target_temp_range_bad_attr(hass):
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24.0
with pytest.raises(vol.Invalid):
- await common.async_set_temperature(
- hass,
- temperature=None,
- entity_id=ENTITY_ECOBEE,
- target_temp_low=None,
- target_temp_high=None,
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ ATTR_ENTITY_ID: ENTITY_ECOBEE,
+ ATTR_TARGET_TEMP_LOW: None,
+ ATTR_TARGET_TEMP_HIGH: None,
+ },
+ blocking=True,
)
- await hass.async_block_till_done()
state = hass.states.get(ENTITY_ECOBEE)
assert state.attributes.get(ATTR_TEMPERATURE) is None
@@ -159,8 +193,12 @@ async def test_set_target_humidity_bad_attr(hass):
assert state.attributes.get(ATTR_HUMIDITY) == 67
with pytest.raises(vol.Invalid):
- await common.async_set_humidity(hass, None, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HUMIDITY,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HUMIDITY: None},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_HUMIDITY) == 67
@@ -171,8 +209,12 @@ async def test_set_target_humidity(hass):
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_HUMIDITY) == 67
- await common.async_set_humidity(hass, 64, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HUMIDITY,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HUMIDITY: 64},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_HUMIDITY) == 64.0
@@ -184,8 +226,12 @@ async def test_set_fan_mode_bad_attr(hass):
assert state.attributes.get(ATTR_FAN_MODE) == "On High"
with pytest.raises(vol.Invalid):
- await common.async_set_fan_mode(hass, None, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: None},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_FAN_MODE) == "On High"
@@ -196,8 +242,12 @@ async def test_set_fan_mode(hass):
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_FAN_MODE) == "On High"
- await common.async_set_fan_mode(hass, "On Low", ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: "On Low"},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_FAN_MODE) == "On Low"
@@ -209,8 +259,12 @@ async def test_set_swing_mode_bad_attr(hass):
assert state.attributes.get(ATTR_SWING_MODE) == "Off"
with pytest.raises(vol.Invalid):
- await common.async_set_swing_mode(hass, None, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_SWING_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: None},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_SWING_MODE) == "Off"
@@ -221,8 +275,12 @@ async def test_set_swing(hass):
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_SWING_MODE) == "Off"
- await common.async_set_swing_mode(hass, "Auto", ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_SWING_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "Auto"},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_SWING_MODE) == "Auto"
@@ -238,8 +296,12 @@ async def test_set_hvac_bad_attr_and_state(hass):
assert state.state == HVAC_MODE_COOL
with pytest.raises(vol.Invalid):
- await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: None},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL
@@ -251,8 +313,12 @@ async def test_set_hvac(hass):
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_COOL
- await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_HEAT
@@ -260,8 +326,12 @@ async def test_set_hvac(hass):
async def test_set_hold_mode_away(hass):
"""Test setting the hold mode away."""
- await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_ECOBEE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ECOBEE, ATTR_PRESET_MODE: PRESET_AWAY},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_ECOBEE)
assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY
@@ -269,8 +339,12 @@ async def test_set_hold_mode_away(hass):
async def test_set_hold_mode_eco(hass):
"""Test setting the hold mode eco."""
- await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_ECOBEE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ECOBEE, ATTR_PRESET_MODE: PRESET_ECO},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_ECOBEE)
assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO
@@ -282,16 +356,25 @@ async def test_set_aux_heat_bad_attr(hass):
assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF
with pytest.raises(vol.Invalid):
- await common.async_set_aux_heat(hass, None, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_AUX_HEAT,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: None},
+ blocking=True,
+ )
+ state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF
async def test_set_aux_heat_on(hass):
"""Test setting the axillary heater on/true."""
- await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_AUX_HEAT,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: True},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_AUX_HEAT) == STATE_ON
@@ -299,8 +382,12 @@ async def test_set_aux_heat_on(hass):
async def test_set_aux_heat_off(hass):
"""Test setting the auxiliary heater off/false."""
- await common.async_set_aux_heat(hass, False, ENTITY_CLIMATE)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_AUX_HEAT,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: False},
+ blocking=True,
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF
@@ -308,21 +395,37 @@ async def test_set_aux_heat_off(hass):
async def test_turn_on(hass):
"""Test turn on device."""
- await common.async_set_hvac_mode(hass, HVAC_MODE_OFF, ENTITY_CLIMATE)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_OFF
- await common.async_turn_on(hass, ENTITY_CLIMATE)
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_HEAT
async def test_turn_off(hass):
"""Test turn on device."""
- await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_HEAT
- await common.async_turn_off(hass, ENTITY_CLIMATE)
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True
+ )
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == HVAC_MODE_OFF
diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py
index d9c9ac77dc84b1..2dca57d3e6b233 100644
--- a/tests/components/demo/test_fan.py
+++ b/tests/components/demo/test_fan.py
@@ -2,19 +2,19 @@
import pytest
from homeassistant.components import fan
-from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ENTITY_MATCH_ALL,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+)
from homeassistant.setup import async_setup_component
-from tests.components.fan import common
-
FAN_ENTITY_ID = "fan.living_room_fan"
-def get_entity(hass):
- """Get the fan entity."""
- return hass.states.get(FAN_ENTITY_ID)
-
-
@pytest.fixture(autouse=True)
async def setup_comp(hass):
"""Initialize components."""
@@ -24,68 +24,122 @@ async def setup_comp(hass):
async def test_turn_on(hass):
"""Test turning on the device."""
- assert STATE_OFF == get_entity(hass).state
-
- await common.async_turn_on(hass, FAN_ENTITY_ID)
- assert STATE_OFF != get_entity(hass).state
-
- await common.async_turn_on(hass, FAN_ENTITY_ID, fan.SPEED_HIGH)
- assert STATE_ON == get_entity(hass).state
- assert fan.SPEED_HIGH == get_entity(hass).attributes[fan.ATTR_SPEED]
+ 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}, blocking=True
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_ON
+
+ 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
async def test_turn_off(hass):
"""Test turning off the device."""
- assert STATE_OFF == get_entity(hass).state
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
- await common.async_turn_on(hass, FAN_ENTITY_ID)
- assert STATE_OFF != get_entity(hass).state
+ await hass.services.async_call(
+ fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_ON
- await common.async_turn_off(hass, FAN_ENTITY_ID)
- assert STATE_OFF == get_entity(hass).state
+ await hass.services.async_call(
+ fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
async def test_turn_off_without_entity_id(hass):
"""Test turning off all fans."""
- assert STATE_OFF == get_entity(hass).state
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
- await common.async_turn_on(hass, FAN_ENTITY_ID)
- assert STATE_OFF != get_entity(hass).state
+ await hass.services.async_call(
+ fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_ON
- await common.async_turn_off(hass)
- assert STATE_OFF == get_entity(hass).state
+ await hass.services.async_call(
+ fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
async def test_set_direction(hass):
"""Test setting the direction of the device."""
- assert STATE_OFF == get_entity(hass).state
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
- await common.async_set_direction(hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE)
- assert fan.DIRECTION_REVERSE == get_entity(hass).attributes.get("direction")
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_SET_DIRECTION,
+ {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_DIRECTION: fan.DIRECTION_REVERSE},
+ blocking=True,
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE
async def test_set_speed(hass):
"""Test setting the speed of the device."""
- assert STATE_OFF == get_entity(hass).state
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
- await common.async_set_speed(hass, FAN_ENTITY_ID, fan.SPEED_LOW)
- assert fan.SPEED_LOW == get_entity(hass).attributes.get("speed")
+ 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
async def test_oscillate(hass):
"""Test oscillating the fan."""
- assert not get_entity(hass).attributes.get("oscillating")
-
- await common.async_oscillate(hass, FAN_ENTITY_ID, True)
- assert get_entity(hass).attributes.get("oscillating")
-
- await common.async_oscillate(hass, FAN_ENTITY_ID, False)
- assert not get_entity(hass).attributes.get("oscillating")
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(fan.ATTR_OSCILLATING)
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_OSCILLATE,
+ {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: True},
+ blocking=True,
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.attributes[fan.ATTR_OSCILLATING] is True
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_OSCILLATE,
+ {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: False},
+ blocking=True,
+ )
+ state = hass.states.get(FAN_ENTITY_ID)
+ assert state.attributes[fan.ATTR_OSCILLATING] is False
async def test_is_on(hass):
"""Test is on service call."""
assert not fan.is_on(hass, FAN_ENTITY_ID)
- await common.async_turn_on(hass, FAN_ENTITY_ID)
+ await hass.services.async_call(
+ fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
+ )
assert fan.is_on(hass, FAN_ENTITY_ID)
diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py
index 7ca870bc34f62a..ac32fff075f8c7 100644
--- a/tests/components/demo/test_geo_location.py
+++ b/tests/components/demo/test_geo_location.py
@@ -8,7 +8,12 @@
DEFAULT_UPDATE_INTERVAL,
NUMBER_OF_DEMO_DEVICES,
)
-from homeassistant.const import LENGTH_KILOMETERS
+from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ ATTR_UNIT_OF_MEASUREMENT,
+ LENGTH_KILOMETERS,
+)
from homeassistant.setup import setup_component
import homeassistant.util.dt as dt_util
@@ -59,13 +64,14 @@ def test_setup_platform(self):
# ignore home zone state
continue
assert (
- abs(state.attributes["latitude"] - self.hass.config.latitude) < 1.0
+ abs(state.attributes[ATTR_LATITUDE] - self.hass.config.latitude)
+ < 1.0
)
assert (
- abs(state.attributes["longitude"] - self.hass.config.longitude)
+ abs(state.attributes[ATTR_LONGITUDE] - self.hass.config.longitude)
< 1.0
)
- assert state.attributes["unit_of_measurement"] == LENGTH_KILOMETERS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_KILOMETERS
# Update (replaces 1 device).
fire_time_changed(self.hass, utcnow + DEFAULT_UPDATE_INTERVAL)
diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py
index 1ab8195c4db962..0d4d1d6bbe40c4 100644
--- a/tests/components/demo/test_media_player.py
+++ b/tests/components/demo/test_media_player.py
@@ -3,11 +3,18 @@
import voluptuous as vol
import homeassistant.components.media_player as mp
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_ENTITY_PICTURE,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_OFF,
+ STATE_PAUSED,
+ STATE_PLAYING,
+)
from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.components.media_player import common
TEST_ENTITY_ID = "media_player.walkman"
@@ -31,16 +38,26 @@ async def test_source_select(hass):
)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert state.attributes.get("source") == "dvd"
+ assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "dvd"
with pytest.raises(vol.Invalid):
- await common.async_select_source(hass, None, entity_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_SELECT_SOURCE,
+ {ATTR_ENTITY_ID: entity_id, mp.ATTR_INPUT_SOURCE: None},
+ blocking=True,
+ )
state = hass.states.get(entity_id)
- assert state.attributes.get("source") == "dvd"
+ assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "dvd"
- await common.async_select_source(hass, "xbox", entity_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_SELECT_SOURCE,
+ {ATTR_ENTITY_ID: entity_id, mp.ATTR_INPUT_SOURCE: "xbox"},
+ blocking=True,
+ )
state = hass.states.get(entity_id)
- assert state.attributes.get("source") == "xbox"
+ assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "xbox"
async def test_clear_playlist(hass):
@@ -49,10 +66,18 @@ async def test_clear_playlist(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
- await common.async_clear_playlist(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "off")
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_CLEAR_PLAYLIST,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_OFF
async def test_volume_services(hass):
@@ -61,36 +86,70 @@ async def test_volume_services(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
+
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("volume_level") == 1.0
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 1.0
with pytest.raises(vol.Invalid):
- await common.async_set_volume_level(hass, None, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_SET,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_LEVEL: None},
+ blocking=True,
+ )
+
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("volume_level") == 1.0
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 1.0
- await common.async_set_volume_level(hass, 0.5, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_SET,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_LEVEL: 0.5},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("volume_level") == 0.5
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.5
- await common.async_volume_down(hass, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_DOWN,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("volume_level") == 0.4
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.4
- await common.async_volume_up(hass, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_UP,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("volume_level") == 0.5
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.5
- assert False is state.attributes.get("is_volume_muted")
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is False
with pytest.raises(vol.Invalid):
- await common.async_mute_volume(hass, None, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_MUTE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_MUTED: None},
+ blocking=True,
+ )
+
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("is_volume_muted") is False
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is False
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_VOLUME_MUTE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_MUTED: True},
+ blocking=True,
+ )
- await common.async_mute_volume(hass, True, TEST_ENTITY_ID)
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("is_volume_muted") is True
+ assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is True
async def test_turning_off_and_on(hass):
@@ -99,17 +158,38 @@ async def test_turning_off_and_on(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
- await common.async_turn_off(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "off")
- assert not mp.is_on(hass, TEST_ENTITY_ID)
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
- await common.async_turn_on(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_OFF
+ assert not mp.is_on(hass, TEST_ENTITY_ID)
- await common.async_toggle(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "off")
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
+ assert mp.is_on(hass, TEST_ENTITY_ID)
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_OFF
assert not mp.is_on(hass, TEST_ENTITY_ID)
@@ -119,19 +199,45 @@ async def test_playing_pausing(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
- await common.async_media_pause(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "paused")
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
- await common.async_media_play_pause(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PAUSE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PAUSED
- await common.async_media_play_pause(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "paused")
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PLAY_PAUSE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
- await common.async_media_play(hass, TEST_ENTITY_ID)
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PLAY_PAUSE,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PAUSED
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PLAY,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
async def test_prev_next_track(hass):
@@ -140,36 +246,63 @@ async def test_prev_next_track(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
+
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("media_track") == 1
+ assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 1
- await common.async_media_next_track(hass, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_NEXT_TRACK,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("media_track") == 2
+ assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 2
- await common.async_media_next_track(hass, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_NEXT_TRACK,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("media_track") == 3
+ assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 3
- await common.async_media_previous_track(hass, TEST_ENTITY_ID)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PREVIOUS_TRACK,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
state = hass.states.get(TEST_ENTITY_ID)
- assert state.attributes.get("media_track") == 2
+ assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 2
assert await async_setup_component(
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
+
ent_id = "media_player.lounge_room"
state = hass.states.get(ent_id)
- assert state.attributes.get("media_episode") == 1
+ assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 1
- await common.async_media_next_track(hass, ent_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_NEXT_TRACK,
+ {ATTR_ENTITY_ID: ent_id},
+ blocking=True,
+ )
state = hass.states.get(ent_id)
- assert state.attributes.get("media_episode") == 2
+ assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 2
- await common.async_media_previous_track(hass, ent_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_PREVIOUS_TRACK,
+ {ATTR_ENTITY_ID: ent_id},
+ blocking=True,
+ )
state = hass.states.get(ent_id)
- assert state.attributes.get("media_episode") == 1
+ assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == 1
async def test_play_media(hass):
@@ -178,21 +311,36 @@ async def test_play_media(hass):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
+
ent_id = "media_player.living_room"
state = hass.states.get(ent_id)
- assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get("supported_features") > 0
- assert state.attributes.get("media_content_id") is not None
+ assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0
+ assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) is not None
with pytest.raises(vol.Invalid):
- await common.async_play_media(hass, None, "some_id", ent_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_PLAY_MEDIA,
+ {ATTR_ENTITY_ID: ent_id, mp.ATTR_MEDIA_CONTENT_ID: "some_id"},
+ blocking=True,
+ )
state = hass.states.get(ent_id)
- assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get("supported_features") > 0
- assert state.attributes.get("media_content_id") != "some_id"
-
- await common.async_play_media(hass, "youtube", "some_id", ent_id)
+ assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0
+ assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) != "some_id"
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_PLAY_MEDIA,
+ {
+ ATTR_ENTITY_ID: ent_id,
+ mp.ATTR_MEDIA_CONTENT_TYPE: "youtube",
+ mp.ATTR_MEDIA_CONTENT_ID: "some_id",
+ },
+ blocking=True,
+ )
state = hass.states.get(ent_id)
- assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get("supported_features") > 0
- assert state.attributes.get("media_content_id") == "some_id"
+ assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0
+ assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) == "some_id"
async def test_seek(hass, mock_media_seek):
@@ -201,15 +349,33 @@ async def test_seek(hass, mock_media_seek):
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
+
ent_id = "media_player.living_room"
state = hass.states.get(ent_id)
- assert state.attributes["supported_features"] & mp.SUPPORT_SEEK
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] & mp.SUPPORT_SEEK
assert not mock_media_seek.called
+
with pytest.raises(vol.Invalid):
- await common.async_media_seek(hass, None, ent_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_SEEK,
+ {
+ ATTR_ENTITY_ID: ent_id,
+ mp.ATTR_MEDIA_SEEK_POSITION: None,
+ },
+ blocking=True,
+ )
assert not mock_media_seek.called
- await common.async_media_seek(hass, 100, ent_id)
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_MEDIA_SEEK,
+ {
+ ATTR_ENTITY_ID: ent_id,
+ mp.ATTR_MEDIA_SEEK_POSITION: 100,
+ },
+ blocking=True,
+ )
assert mock_media_seek.called
@@ -249,9 +415,9 @@ def detach(self):
hass.data[DATA_CLIENTSESSION] = MockWebsession()
- assert hass.states.is_state(TEST_ENTITY_ID, "playing")
state = hass.states.get(TEST_ENTITY_ID)
+ assert state.state == STATE_PLAYING
client = await hass_client()
- req = await client.get(state.attributes.get("entity_picture"))
+ req = await client.get(state.attributes.get(ATTR_ENTITY_PICTURE))
assert req.status == 200
assert await req.text() == fake_picture_data
diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py
index 19715577e2ae13..4879be9d18c6d8 100644
--- a/tests/components/device_sun_light_trigger/test_init.py
+++ b/tests/components/device_sun_light_trigger/test_init.py
@@ -176,6 +176,8 @@ async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanne
{"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]},
)
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(hass, "person_me", ["person.me"])
assert await async_setup_component(
diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py
index 6790c85b777137..89e87c78c64303 100644
--- a/tests/components/devolo_home_control/test_config_flow.py
+++ b/tests/components/devolo_home_control/test_config_flow.py
@@ -66,7 +66,7 @@ async def test_form_invalid_credentials(hass):
{"username": "test-username", "password": "test-password"},
)
- assert result["errors"] == {"base": "invalid_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
async def test_form_already_configured(hass):
diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py
index b95f796b230ccc..6f6d8abe8f9309 100644
--- a/tests/components/dexcom/test_config_flow.py
+++ b/tests/components/dexcom/test_config_flow.py
@@ -57,7 +57,7 @@ async def test_form_account_error(hass):
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["errors"] == {"base": "account_error"}
+ assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_session_error(hass):
@@ -76,7 +76,7 @@ async def test_form_session_error(hass):
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["errors"] == {"base": "session_error"}
+ assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass):
diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py
index 6b71b1e8cfeda1..9f09c377bd403a 100644
--- a/tests/components/directv/__init__.py
+++ b/tests/components/directv/__init__.py
@@ -1,7 +1,12 @@
"""Tests for the DirecTV component."""
from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION
-from homeassistant.const import CONF_HOST, HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR
+from homeassistant.const import (
+ CONF_HOST,
+ CONTENT_TYPE_JSON,
+ HTTP_FORBIDDEN,
+ HTTP_INTERNAL_SERVER_ERROR,
+)
from homeassistant.helpers.typing import HomeAssistantType
from tests.common import MockConfigEntry, load_fixture
@@ -22,20 +27,20 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None:
aioclient_mock.get(
f"http://{HOST}:8080/info/getVersion",
text=load_fixture("directv/info-get-version.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/info/getLocations",
text=load_fixture("directv/info-get-locations.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/info/mode",
params={"clientAddr": "B01234567890"},
text=load_fixture("directv/info-mode-standby.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -43,39 +48,39 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None:
params={"clientAddr": "9XXXXXXXXXX9"},
status=HTTP_INTERNAL_SERVER_ERROR,
text=load_fixture("directv/info-mode-error.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/info/mode",
text=load_fixture("directv/info-mode.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/remote/processKey",
text=load_fixture("directv/remote-process-key.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/tv/tune",
text=load_fixture("directv/tv-tune.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/tv/getTuned",
params={"clientAddr": "2CA17D1CD30X"},
text=load_fixture("directv/tv-get-tuned.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/tv/getTuned",
params={"clientAddr": "A01234567890"},
text=load_fixture("directv/tv-get-tuned-music.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -83,13 +88,13 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None:
params={"clientAddr": "C01234567890"},
status=HTTP_FORBIDDEN,
text=load_fixture("directv/tv-get-tuned-restricted.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"http://{HOST}:8080/tv/getTuned",
text=load_fixture("directv/tv-get-tuned-movie.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py
index 26f79e3e62fa19..8003f83d99649b 100644
--- a/tests/components/discovery/test_init.py
+++ b/tests/components/discovery/test_init.py
@@ -37,7 +37,9 @@ def netdisco_mock():
async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
"""Mock discoveries."""
- with patch("homeassistant.components.zeroconf.async_get_instance"):
+ with patch("homeassistant.components.zeroconf.async_get_instance"), patch(
+ "homeassistant.components.zeroconf.async_setup", return_value=True
+ ):
assert await async_setup_component(hass, "discovery", config)
await hass.async_block_till_done()
await hass.async_start()
diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py
new file mode 100644
index 00000000000000..d2cec93df95288
--- /dev/null
+++ b/tests/components/dsmr/conftest.py
@@ -0,0 +1,71 @@
+"""Common test tools."""
+import asyncio
+
+from dsmr_parser.clients.protocol import DSMRProtocol
+from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS
+from dsmr_parser.objects import CosemObject
+import pytest
+
+from tests.async_mock import MagicMock, patch
+
+
+@pytest.fixture
+async def dsmr_connection_fixture(hass):
+ """Fixture that mocks serial connection."""
+
+ transport = MagicMock(spec=asyncio.Transport)
+ protocol = MagicMock(spec=DSMRProtocol)
+
+ 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_dsmr_reader", connection_factory
+ ), patch(
+ "homeassistant.components.dsmr.sensor.create_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."""
+
+ transport = MagicMock(spec=asyncio.Transport)
+ protocol = MagicMock(spec=DSMRProtocol)
+
+ async def connection_factory(*args, **kwargs):
+ """Return mocked out Asyncio classes."""
+ return (transport, protocol)
+
+ connection_factory = MagicMock(wraps=connection_factory)
+
+ protocol.telegram = {
+ EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]),
+ EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]),
+ }
+
+ 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_dsmr_reader",
+ connection_factory,
+ ), patch(
+ "homeassistant.components.dsmr.config_flow.create_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 c35562b4024198..c6fb57c6ecbc4d 100644
--- a/tests/components/dsmr/test_config_flow.py
+++ b/tests/components/dsmr/test_config_flow.py
@@ -2,64 +2,18 @@
import asyncio
from itertools import chain, repeat
-from dsmr_parser.clients.protocol import DSMRProtocol
-from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS
-from dsmr_parser.objects import CosemObject
-import pytest
import serial
from homeassistant import config_entries, setup
from homeassistant.components.dsmr import DOMAIN
-from tests.async_mock import DEFAULT, AsyncMock, Mock, patch
+from tests.async_mock import DEFAULT, AsyncMock, patch
from tests.common import MockConfigEntry
SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"}
-@pytest.fixture
-def mock_connection_factory(monkeypatch):
- """Mock the create functions for serial and TCP Asyncio connections."""
- transport = Mock(spec=asyncio.Transport)
- protocol = Mock(spec=DSMRProtocol)
-
- async def connection_factory(*args, **kwargs):
- """Return mocked out Asyncio classes."""
- return (transport, protocol)
-
- connection_factory = Mock(wraps=connection_factory)
-
- # apply the mock to both connection factories
- monkeypatch.setattr(
- "homeassistant.components.dsmr.config_flow.create_dsmr_reader",
- connection_factory,
- )
- monkeypatch.setattr(
- "homeassistant.components.dsmr.config_flow.create_tcp_dsmr_reader",
- connection_factory,
- )
-
- protocol.telegram = {
- EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]),
- EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]),
- }
-
- 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
-
- return connection_factory, transport, protocol
-
-
-async def test_import_usb(hass, mock_connection_factory):
+async def test_import_usb(hass, dsmr_connection_send_validate_fixture):
"""Test we can import."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -82,9 +36,11 @@ async def test_import_usb(hass, mock_connection_factory):
assert result["data"] == {**entry_data, **SERIAL_DATA}
-async def test_import_usb_failed_connection(hass, monkeypatch, mock_connection_factory):
+async def test_import_usb_failed_connection(
+ hass, dsmr_connection_send_validate_fixture
+):
"""Test we can import."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -101,12 +57,12 @@ async def test_import_usb_failed_connection(hass, monkeypatch, mock_connection_f
side_effect=chain([serial.serialutil.SerialException], repeat(DEFAULT)),
)
- monkeypatch.setattr(
+ with patch(
+ "homeassistant.components.dsmr.async_setup_entry", return_value=True
+ ), patch(
"homeassistant.components.dsmr.config_flow.create_dsmr_reader",
first_fail_connection_factory,
- )
-
- with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
+ ):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@@ -117,9 +73,9 @@ async def test_import_usb_failed_connection(hass, monkeypatch, mock_connection_f
assert result["reason"] == "cannot_connect"
-async def test_import_usb_no_data(hass, monkeypatch, mock_connection_factory):
+async def test_import_usb_no_data(hass, dsmr_connection_send_validate_fixture):
"""Test we can import."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -149,9 +105,9 @@ async def test_import_usb_no_data(hass, monkeypatch, mock_connection_factory):
assert result["reason"] == "cannot_communicate"
-async def test_import_usb_wrong_telegram(hass, mock_connection_factory):
+async def test_import_usb_wrong_telegram(hass, dsmr_connection_send_validate_fixture):
"""Test we can import."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -175,7 +131,7 @@ async def test_import_usb_wrong_telegram(hass, mock_connection_factory):
assert result["reason"] == "cannot_communicate"
-async def test_import_network(hass, mock_connection_factory):
+async def test_import_network(hass, dsmr_connection_send_validate_fixture):
"""Test we can import from network."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -199,7 +155,7 @@ async def test_import_network(hass, mock_connection_factory):
assert result["data"] == {**entry_data, **SERIAL_DATA}
-async def test_import_update(hass, mock_connection_factory):
+async def test_import_update(hass, dsmr_connection_send_validate_fixture):
"""Test we can import."""
await setup.async_setup_component(hass, "persistent_notification", {})
diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py
index f0ff2f85c57edd..49e9feb80f6c52 100644
--- a/tests/components/dsmr/test_sensor.py
+++ b/tests/components/dsmr/test_sensor.py
@@ -10,46 +10,17 @@
from decimal import Decimal
from itertools import chain, repeat
-import pytest
-
from homeassistant.components.dsmr.const import DOMAIN
from homeassistant.components.dsmr.sensor import DerivativeDSMREntity
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS
from homeassistant.setup import async_setup_component
-import tests.async_mock
-from tests.async_mock import DEFAULT, MagicMock, Mock
+from tests.async_mock import DEFAULT, MagicMock
from tests.common import MockConfigEntry, patch
-@pytest.fixture
-def mock_connection_factory(monkeypatch):
- """Mock the create functions for serial and TCP Asyncio connections."""
- from dsmr_parser.clients.protocol import DSMRProtocol
-
- transport = tests.async_mock.Mock(spec=asyncio.Transport)
- protocol = tests.async_mock.Mock(spec=DSMRProtocol)
-
- async def connection_factory(*args, **kwargs):
- """Return mocked out Asyncio classes."""
- return (transport, protocol)
-
- connection_factory = Mock(wraps=connection_factory)
-
- # apply the mock to both connection factories
- monkeypatch.setattr(
- "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory
- )
- monkeypatch.setattr(
- "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader",
- connection_factory,
- )
-
- return connection_factory, transport, protocol
-
-
-async def test_setup_platform(hass, mock_connection_factory):
+async def test_setup_platform(hass, dsmr_connection_fixture):
"""Test setup of platform."""
async_add_entities = MagicMock()
@@ -87,9 +58,9 @@ async def test_setup_platform(hass, mock_connection_factory):
assert entry.data == {**entry_data, **serial_data}
-async def test_default_setup(hass, mock_connection_factory):
+async def test_default_setup(hass, dsmr_connection_fixture):
"""Test the default setup."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
CURRENT_ELECTRICITY_USAGE,
@@ -157,10 +128,6 @@ async def test_default_setup(hass, mock_connection_factory):
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
- await hass.config_entries.async_unload(mock_entry.entry_id)
-
- assert mock_entry.state == "not_loaded"
-
async def test_derivative():
"""Test calculation of derivative value."""
@@ -202,9 +169,9 @@ async def test_derivative():
assert entity.unit_of_measurement == f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}"
-async def test_v4_meter(hass, mock_connection_factory):
+async def test_v4_meter(hass, dsmr_connection_fixture):
"""Test if v4 meter is correctly parsed."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
ELECTRICITY_ACTIVE_TARIFF,
@@ -256,14 +223,10 @@ async def test_v4_meter(hass, mock_connection_factory):
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
- await hass.config_entries.async_unload(mock_entry.entry_id)
-
- assert mock_entry.state == "not_loaded"
-
-async def test_v5_meter(hass, mock_connection_factory):
+async def test_v5_meter(hass, dsmr_connection_fixture):
"""Test if v5 meter is correctly parsed."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
ELECTRICITY_ACTIVE_TARIFF,
@@ -315,14 +278,10 @@ async def test_v5_meter(hass, mock_connection_factory):
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
- await hass.config_entries.async_unload(mock_entry.entry_id)
-
- assert mock_entry.state == "not_loaded"
-
-async def test_belgian_meter(hass, mock_connection_factory):
+async def test_belgian_meter(hass, dsmr_connection_fixture):
"""Test if Belgian meter is correctly parsed."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
BELGIUM_HOURLY_GAS_METER_READING,
@@ -374,14 +333,10 @@ async def test_belgian_meter(hass, mock_connection_factory):
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
- await hass.config_entries.async_unload(mock_entry.entry_id)
-
- assert mock_entry.state == "not_loaded"
-
-async def test_belgian_meter_low(hass, mock_connection_factory):
+async def test_belgian_meter_low(hass, dsmr_connection_fixture):
"""Test if Belgian meter is correctly parsed."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF
from dsmr_parser.objects import CosemObject
@@ -417,14 +372,10 @@ async def test_belgian_meter_low(hass, mock_connection_factory):
assert power_tariff.state == "low"
assert power_tariff.attributes.get("unit_of_measurement") == ""
- await hass.config_entries.async_unload(mock_entry.entry_id)
-
- assert mock_entry.state == "not_loaded"
-
-async def test_tcp(hass, mock_connection_factory):
+async def test_tcp(hass, dsmr_connection_fixture):
"""If proper config provided TCP connection should be made."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
entry_data = {
"host": "localhost",
@@ -446,14 +397,10 @@ async def test_tcp(hass, mock_connection_factory):
assert connection_factory.call_args_list[0][0][0] == "localhost"
assert connection_factory.call_args_list[0][0][1] == "1234"
- await hass.config_entries.async_unload(mock_entry.entry_id)
- assert mock_entry.state == "not_loaded"
-
-
-async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory):
+async def test_connection_errors_retry(hass, dsmr_connection_fixture):
"""Connection should be retried on error during setup."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
entry_data = {
"port": "/dev/ttyUSB0",
@@ -463,37 +410,32 @@ async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factor
}
# override the mock to have it fail the first time and succeed after
- first_fail_connection_factory = tests.async_mock.AsyncMock(
+ first_fail_connection_factory = MagicMock(
return_value=(transport, protocol),
side_effect=chain([TimeoutError], repeat(DEFAULT)),
)
- monkeypatch.setattr(
- "homeassistant.components.dsmr.sensor.create_dsmr_reader",
- first_fail_connection_factory,
- )
-
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()
-
- # wait for sleep to resolve
- await hass.async_block_till_done()
- assert first_fail_connection_factory.call_count >= 2, "connecting not retried"
-
- await hass.config_entries.async_unload(mock_entry.entry_id)
+ with patch(
+ "homeassistant.components.dsmr.sensor.create_dsmr_reader",
+ first_fail_connection_factory,
+ ):
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
- assert mock_entry.state == "not_loaded"
+ # wait for sleep to resolve
+ await hass.async_block_till_done()
+ assert first_fail_connection_factory.call_count >= 2, "connecting not retried"
-async def test_reconnect(hass, monkeypatch, mock_connection_factory):
+async def test_reconnect(hass, dsmr_connection_fixture):
"""If transport disconnects, the connection should be retried."""
- (connection_factory, transport, protocol) = mock_connection_factory
+ (connection_factory, transport, protocol) = dsmr_connection_fixture
entry_data = {
"port": "/dev/ttyUSB0",
diff --git a/tests/components/dte_energy_bridge/test_sensor.py b/tests/components/dte_energy_bridge/test_sensor.py
index 38d712468c0a86..b0d2b2b7b2d331 100644
--- a/tests/components/dte_energy_bridge/test_sensor.py
+++ b/tests/components/dte_energy_bridge/test_sensor.py
@@ -1,65 +1,56 @@
"""The tests for the DTE Energy Bridge."""
-
-import unittest
-
import requests_mock
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
+from homeassistant.setup import async_setup_component
DTE_ENERGY_BRIDGE_CONFIG = {"platform": "dte_energy_bridge", "ip": "192.168.1.1"}
-class TestDteEnergyBridgeSetup(unittest.TestCase):
- """Test the DTE Energy Bridge platform."""
+async def test_setup_with_config(hass):
+ """Test the platform setup with configuration."""
+ assert await async_setup_component(
+ hass, "sensor", {"dte_energy_bridge": DTE_ENERGY_BRIDGE_CONFIG}
+ )
+ await hass.async_block_till_done()
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
- def test_setup_with_config(self):
- """Test the platform setup with configuration."""
- assert setup_component(
- self.hass, "sensor", {"dte_energy_bridge": DTE_ENERGY_BRIDGE_CONFIG}
- )
-
- @requests_mock.Mocker()
- def test_setup_correct_reading(self, mock_req):
- """Test DTE Energy bridge returns a correct value."""
+async def test_setup_correct_reading(hass):
+ """Test DTE Energy bridge returns a correct value."""
+ with requests_mock.Mocker() as mock_req:
mock_req.get(
"http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]),
text=".411 kW",
)
- assert setup_component(
- self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
+ assert await async_setup_component(
+ hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
)
- self.hass.block_till_done()
- assert "0.411" == self.hass.states.get("sensor.current_energy_usage").state
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.current_energy_usage").state == "0.411"
+
- @requests_mock.Mocker()
- def test_setup_incorrect_units_reading(self, mock_req):
- """Test DTE Energy bridge handles a value with incorrect units."""
+async def test_setup_incorrect_units_reading(hass):
+ """Test DTE Energy bridge handles a value with incorrect units."""
+ with requests_mock.Mocker() as mock_req:
mock_req.get(
"http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]),
text="411 kW",
)
- assert setup_component(
- self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
+ assert await async_setup_component(
+ hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
)
- self.hass.block_till_done()
- assert "0.411" == self.hass.states.get("sensor.current_energy_usage").state
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.current_energy_usage").state == "0.411"
+
- @requests_mock.Mocker()
- def test_setup_bad_format_reading(self, mock_req):
- """Test DTE Energy bridge handles an invalid value."""
+async def test_setup_bad_format_reading(hass):
+ """Test DTE Energy bridge handles an invalid value."""
+ with requests_mock.Mocker() as mock_req:
mock_req.get(
"http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]),
text="411",
)
- assert setup_component(
- self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
+ assert await async_setup_component(
+ hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG}
)
- self.hass.block_till_done()
- assert "unknown" == self.hass.states.get("sensor.current_energy_usage").state
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.current_energy_usage").state == "unknown"
diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py
index cef4081c60714b..4f696d905d3570 100644
--- a/tests/components/dynalite/test_cover.py
+++ b/tests/components/dynalite/test_cover.py
@@ -2,6 +2,8 @@
from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice
import pytest
+from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME
+
from .common import (
ATTR_ARGS,
ATTR_METHOD,
@@ -24,7 +26,7 @@ async def test_cover_setup(hass, mock_device):
"""Test a successful setup."""
await create_entity_from_device(hass, mock_device)
entity_state = hass.states.get("cover.name")
- assert entity_state.attributes["friendly_name"] == mock_device.name
+ assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name
assert (
entity_state.attributes["current_position"]
== mock_device.current_cover_position
@@ -33,7 +35,7 @@ async def test_cover_setup(hass, mock_device):
entity_state.attributes["current_tilt_position"]
== mock_device.current_cover_tilt_position
)
- assert entity_state.attributes["device_class"] == mock_device.device_class
+ assert entity_state.attributes[ATTR_DEVICE_CLASS] == mock_device.device_class
await run_service_tests(
hass,
mock_device,
diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py
index deea32d2e3410f..7df10fb08e8e45 100644
--- a/tests/components/dynalite/test_light.py
+++ b/tests/components/dynalite/test_light.py
@@ -4,6 +4,7 @@
import pytest
from homeassistant.components.light import SUPPORT_BRIGHTNESS
+from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES
from .common import (
ATTR_METHOD,
@@ -25,9 +26,9 @@ async def test_light_setup(hass, mock_device):
"""Test a successful setup."""
await create_entity_from_device(hass, mock_device)
entity_state = hass.states.get("light.name")
- assert entity_state.attributes["friendly_name"] == mock_device.name
+ assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name
assert entity_state.attributes["brightness"] == mock_device.brightness
- assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS
+ assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_BRIGHTNESS
await run_service_tests(
hass,
mock_device,
diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py
index 7c0c5d632d3c09..de375e3b348d7f 100644
--- a/tests/components/dynalite/test_switch.py
+++ b/tests/components/dynalite/test_switch.py
@@ -3,6 +3,8 @@
from dynalite_devices_lib.switch import DynalitePresetSwitchDevice
import pytest
+from homeassistant.const import ATTR_FRIENDLY_NAME
+
from .common import (
ATTR_METHOD,
ATTR_SERVICE,
@@ -22,7 +24,7 @@ async def test_switch_setup(hass, mock_device):
"""Test a successful setup."""
await create_entity_from_device(hass, mock_device)
entity_state = hass.states.get("switch.name")
- assert entity_state.attributes["friendly_name"] == mock_device.name
+ assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name
await run_service_tests(
hass,
mock_device,
diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py
index cca589875aa1a8..77105dc73db6e7 100644
--- a/tests/components/dyson/test_climate.py
+++ b/tests/components/dyson/test_climate.py
@@ -1,6 +1,5 @@
"""Test the Dyson fan component."""
import json
-import unittest
from libpurecool.const import (
FanPower,
@@ -15,8 +14,8 @@
from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink
from libpurecool.dyson_pure_state import DysonPureHotCoolState
from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State
+import pytest
-from homeassistant.components import dyson as dyson_parent
from homeassistant.components.climate import (
DOMAIN,
SERVICE_SET_FAN_MODE,
@@ -25,9 +24,16 @@
)
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
+ ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
+ ATTR_FAN_MODES,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
+ ATTR_HVAC_MODES,
+ ATTR_MAX_TEMP,
+ ATTR_MIN_TEMP,
+ ATTR_TARGET_TEMP_HIGH,
+ ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
@@ -40,31 +46,45 @@
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
)
-from homeassistant.components.dyson import climate as dyson
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN as DYSON_DOMAIN
+from homeassistant.components.dyson.climate import FAN_DIFFUSE, FAN_FOCUS, SUPPORT_FLAGS
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ ATTR_TEMPERATURE,
+ CONF_DEVICES,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ TEMP_CELSIUS,
+)
from homeassistant.setup import async_setup_component
from .common import load_mock_device
-from tests.async_mock import MagicMock, Mock, patch
-from tests.common import get_test_home_assistant
+from tests.async_mock import Mock, call, patch
class MockDysonState(DysonPureHotCoolState):
"""Mock Dyson state."""
+ # pylint: disable=super-init-not-called
+
def __init__(self):
"""Create new Mock Dyson State."""
+ def __repr__(self):
+ """Mock repr because original one fails since constructor not called."""
+ return ""
+
def _get_config():
"""Return a config dictionary."""
return {
- dyson_parent.DOMAIN: {
- dyson_parent.CONF_USERNAME: "email",
- dyson_parent.CONF_PASSWORD: "password",
- dyson_parent.CONF_LANGUAGE: "GB",
- dyson_parent.CONF_DEVICES: [
+ DYSON_DOMAIN: {
+ CONF_USERNAME: "email",
+ CONF_PASSWORD: "password",
+ CONF_LANGUAGE: "GB",
+ CONF_DEVICES: [
{"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"},
{"device_id": "YY-YYYYY-YY", "device_ip": "192.168.0.2"},
],
@@ -85,15 +105,6 @@ def _get_dyson_purehotcool_device():
return device
-def _get_device_with_no_state():
- """Return a device with no state."""
- device = Mock(spec=DysonPureHotCoolLink)
- load_mock_device(device)
- device.state = None
- device.environmental_state = None
- return device
-
-
def _get_device_off():
"""Return a device with state off."""
device = Mock(spec=DysonPureHotCoolLink)
@@ -101,22 +112,6 @@ def _get_device_off():
return device
-def _get_device_focus():
- """Return a device with fan state of focus mode."""
- device = Mock(spec=DysonPureHotCoolLink)
- load_mock_device(device)
- device.state.focus_mode = FocusMode.FOCUS_ON.value
- return device
-
-
-def _get_device_diffuse():
- """Return a device with fan state of diffuse mode."""
- device = Mock(spec=DysonPureHotCoolLink)
- load_mock_device(device)
- device.state.focus_mode = FocusMode.FOCUS_OFF.value
- return device
-
-
def _get_device_cool():
"""Return a device with state of cooling."""
device = Mock(spec=DysonPureHotCoolLink)
@@ -128,15 +123,6 @@ def _get_device_cool():
return device
-def _get_device_heat_off():
- """Return a device with state of heat reached target."""
- device = Mock(spec=DysonPureHotCoolLink)
- load_mock_device(device)
- device.state.heat_mode = HeatMode.HEAT_ON.value
- device.state.heat_state = HeatState.HEAT_STATE_OFF.value
- return device
-
-
def _get_device_heat_on():
"""Return a device with state of heating."""
device = Mock(spec=DysonPureHotCoolLink)
@@ -150,236 +136,259 @@ def _get_device_heat_on():
return device
-class DysonTest(unittest.TestCase):
- """Dyson Climate component test class."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_setup_component_without_devices(self):
- """Test setup component with no devices."""
- self.hass.data[dyson.DYSON_DEVICES] = []
- add_devices = MagicMock()
- dyson.setup_platform(self.hass, None, add_devices)
- add_devices.assert_not_called()
-
- def test_setup_component_with_devices(self):
- """Test setup component with valid devices."""
- devices = [
- _get_device_with_no_state(),
- _get_device_off(),
- _get_device_heat_on(),
- ]
- self.hass.data[dyson.DYSON_DEVICES] = devices
- add_devices = MagicMock()
- dyson.setup_platform(self.hass, None, add_devices, discovery_info={})
- assert add_devices.called
-
- def test_setup_component(self):
- """Test setup component with devices."""
- device_fan = _get_device_heat_on()
- device_non_fan = _get_device_off()
-
- def _add_device(devices):
- assert len(devices) == 1
- assert devices[0].name == "Device_name"
-
- self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan]
- dyson.setup_platform(self.hass, None, _add_device)
-
- def test_dyson_set_temperature(self):
- """Test set climate temperature."""
- device = _get_device_heat_on()
- device.temp_unit = TEMP_CELSIUS
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert not entity.should_poll
-
- # Without target temp.
- kwargs = {}
- entity.set_temperature(**kwargs)
- set_config = device.set_configuration
- set_config.assert_not_called()
-
- kwargs = {ATTR_TEMPERATURE: 23}
- entity.set_temperature(**kwargs)
- set_config = device.set_configuration
- set_config.assert_called_with(
- heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23)
- )
-
- # Should clip the target temperature between 1 and 37 inclusive.
- kwargs = {ATTR_TEMPERATURE: 50}
- entity.set_temperature(**kwargs)
- set_config = device.set_configuration
- set_config.assert_called_with(
- heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(37)
- )
-
- kwargs = {ATTR_TEMPERATURE: -5}
- entity.set_temperature(**kwargs)
- set_config = device.set_configuration
- set_config.assert_called_with(
- heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(1)
- )
-
- def test_dyson_set_temperature_when_cooling_mode(self):
- """Test set climate temperature when heating is off."""
- device = _get_device_cool()
- device.temp_unit = TEMP_CELSIUS
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- entity.schedule_update_ha_state = Mock()
-
- kwargs = {ATTR_TEMPERATURE: 23}
- entity.set_temperature(**kwargs)
- set_config = device.set_configuration
- set_config.assert_called_with(
- heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23)
- )
-
- def test_dyson_set_fan_mode(self):
- """Test set fan mode."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert not entity.should_poll
-
- entity.set_fan_mode(dyson.FAN_FOCUS)
- set_config = device.set_configuration
- set_config.assert_called_with(focus_mode=FocusMode.FOCUS_ON)
-
- entity.set_fan_mode(dyson.FAN_DIFFUSE)
- set_config = device.set_configuration
- set_config.assert_called_with(focus_mode=FocusMode.FOCUS_OFF)
-
- def test_dyson_fan_modes(self):
- """Test get fan list."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert len(entity.fan_modes) == 2
- assert dyson.FAN_FOCUS in entity.fan_modes
- assert dyson.FAN_DIFFUSE in entity.fan_modes
-
- def test_dyson_fan_mode_focus(self):
- """Test fan focus mode."""
- device = _get_device_focus()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.fan_mode == dyson.FAN_FOCUS
-
- def test_dyson_fan_mode_diffuse(self):
- """Test fan diffuse mode."""
- device = _get_device_diffuse()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.fan_mode == dyson.FAN_DIFFUSE
-
- def test_dyson_set_hvac_mode(self):
- """Test set operation mode."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert not entity.should_poll
-
- entity.set_hvac_mode(dyson.HVAC_MODE_HEAT)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON)
-
- entity.set_hvac_mode(dyson.HVAC_MODE_COOL)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF)
-
- def test_dyson_operation_list(self):
- """Test get operation list."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert len(entity.hvac_modes) == 2
- assert dyson.HVAC_MODE_HEAT in entity.hvac_modes
- assert dyson.HVAC_MODE_COOL in entity.hvac_modes
-
- def test_dyson_heat_off(self):
- """Test turn off heat."""
- device = _get_device_heat_off()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- entity.set_hvac_mode(dyson.HVAC_MODE_COOL)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF)
-
- def test_dyson_heat_on(self):
- """Test turn on heat."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- entity.set_hvac_mode(dyson.HVAC_MODE_HEAT)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON)
-
- def test_dyson_heat_value_on(self):
- """Test get heat value on."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.hvac_mode == dyson.HVAC_MODE_HEAT
-
- def test_dyson_heat_value_off(self):
- """Test get heat value off."""
- device = _get_device_cool()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.hvac_mode == dyson.HVAC_MODE_COOL
-
- def test_dyson_heat_value_idle(self):
- """Test get heat value idle."""
- device = _get_device_heat_off()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.hvac_mode == dyson.HVAC_MODE_HEAT
- assert entity.hvac_action == dyson.CURRENT_HVAC_IDLE
-
- def test_on_message(self):
- """Test when message is received."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- entity.schedule_update_ha_state = Mock()
- entity.on_message(MockDysonState())
- entity.schedule_update_ha_state.assert_called_with()
-
- def test_general_properties(self):
- """Test properties of entity."""
- device = _get_device_with_no_state()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.should_poll is False
- assert entity.supported_features == dyson.SUPPORT_FLAGS
- assert entity.temperature_unit == TEMP_CELSIUS
-
- def test_property_current_humidity(self):
- """Test properties of current humidity."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.current_humidity == 53
-
- def test_property_current_humidity_with_invalid_env_state(self):
- """Test properties of current humidity with invalid env state."""
- device = _get_device_off()
- device.environmental_state.humidity = 0
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.current_humidity is None
-
- def test_property_current_humidity_without_env_state(self):
- """Test properties of current humidity without env state."""
- device = _get_device_with_no_state()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.current_humidity is None
-
- def test_property_current_temperature(self):
- """Test properties of current temperature."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- # Result should be in celsius, hence then subtraction of 273.
- assert entity.current_temperature == 289 - 273
-
- def test_property_target_temperature(self):
- """Test properties of target temperature."""
- device = _get_device_heat_on()
- entity = dyson.DysonPureHotCoolLinkEntity(device)
- assert entity.target_temperature == 23
+@pytest.fixture(autouse=True)
+def patch_platforms_fixture():
+ """Only set up the climate platform for the climate tests."""
+ with patch("homeassistant.components.dyson.DYSON_PLATFORMS", new=[DOMAIN]):
+ yield
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[_get_device_heat_on()],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_pure_hot_cool_link_set_mode(mocked_login, mocked_devices, hass):
+ """Test set climate mode."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ device = mocked_devices.return_value[0]
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(heat_mode=HeatMode.HEAT_ON)
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_COOL},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(heat_mode=HeatMode.HEAT_OFF)
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[_get_device_heat_on()],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_pure_hot_cool_link_set_fan(mocked_login, mocked_devices, hass):
+ """Test set climate fan."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ device = mocked_devices.return_value[0]
+ device.temp_unit = TEMP_CELSIUS
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_FOCUS},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_ON)
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_DIFFUSE},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_OFF)
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[_get_device_heat_on()],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_pure_hot_cool_link_state(mocked_login, mocked_devices, hass):
+ """Test set climate temperature."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_FLAGS
+ assert state.attributes[ATTR_TEMPERATURE] == 23
+ assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 289 - 273
+ assert state.attributes[ATTR_CURRENT_HUMIDITY] == 53
+ assert state.state == HVAC_MODE_HEAT
+ assert len(state.attributes[ATTR_HVAC_MODES]) == 2
+ assert HVAC_MODE_HEAT in state.attributes[ATTR_HVAC_MODES]
+ assert HVAC_MODE_COOL in state.attributes[ATTR_HVAC_MODES]
+ assert len(state.attributes[ATTR_FAN_MODES]) == 2
+ assert FAN_FOCUS in state.attributes[ATTR_FAN_MODES]
+ assert FAN_DIFFUSE in state.attributes[ATTR_FAN_MODES]
+
+ device = mocked_devices.return_value[0]
+ update_callback = device.add_message_listener.call_args[0][0]
+
+ device.state.focus_mode = FocusMode.FOCUS_ON.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes[ATTR_FAN_MODE] == FAN_FOCUS
+
+ device.state.focus_mode = FocusMode.FOCUS_OFF.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes[ATTR_FAN_MODE] == FAN_DIFFUSE
+
+ device.state.heat_mode = HeatMode.HEAT_ON.value
+ device.state.heat_state = HeatState.HEAT_STATE_OFF.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
+
+ device.environmental_state.humidity = 0
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
+
+ device.environmental_state = None
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
+
+ device.state.heat_mode = HeatMode.HEAT_OFF.value
+ device.state.heat_state = HeatState.HEAT_STATE_OFF.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
+
+ state = hass.states.get("climate.temp_name")
+ assert state.state == HVAC_MODE_COOL
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_setup_component_without_devices(mocked_login, mocked_devices, hass):
+ """Test setup component with no devices."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ entity_ids = hass.states.async_entity_ids(DOMAIN)
+ assert not entity_ids
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[_get_device_heat_on()],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_dyson_set_temperature(mocked_login, mocked_devices, hass):
+ """Test set climate temperature."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ device = mocked_devices.return_value[0]
+ device.temp_unit = TEMP_CELSIUS
+
+ # Without correct target temp.
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ ATTR_ENTITY_ID: "climate.temp_name",
+ ATTR_TARGET_TEMP_HIGH: 25.0,
+ ATTR_TARGET_TEMP_LOW: 15.0,
+ },
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_count == 0
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(
+ heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23)
+ )
+
+ # Should clip the target temperature between 1 and 37 inclusive.
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 50},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(
+ heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(37)
+ )
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: -5},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(
+ heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(1)
+ )
+
+
+@patch(
+ "homeassistant.components.dyson.DysonAccount.devices",
+ return_value=[_get_device_cool()],
+)
+@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True)
+async def test_dyson_set_temperature_when_cooling_mode(
+ mocked_login, mocked_devices, hass
+):
+ """Test set climate temperature when heating is off."""
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
+ await hass.async_block_till_done()
+
+ device = mocked_devices.return_value[0]
+ device.temp_unit = TEMP_CELSIUS
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23},
+ True,
+ )
+
+ set_config = device.set_configuration
+ assert set_config.call_args == call(
+ heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23)
+ )
@patch(
@@ -391,10 +400,10 @@ async def test_setup_component_with_parent_discovery(
mocked_login, mocked_devices, hass
):
"""Test setup_component using discovery."""
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
- entity_ids = hass.states.async_entity_ids("climate")
+ entity_ids = hass.states.async_entity_ids(DOMAIN)
assert len(entity_ids) == 2
@@ -406,10 +415,10 @@ async def test_setup_component_with_parent_discovery(
async def test_purehotcool_component_setup_only_once(devices, login, hass):
"""Test if entities are created only once."""
config = _get_config()
- await async_setup_component(hass, dyson_parent.DOMAIN, config)
+ await async_setup_component(hass, DYSON_DOMAIN, config)
await hass.async_block_till_done()
- entity_ids = hass.states.async_entity_ids("climate")
+ entity_ids = hass.states.async_entity_ids(DOMAIN)
assert len(entity_ids) == 1
state = hass.states.get(entity_ids[0])
assert state.name == "Living room"
@@ -423,10 +432,10 @@ async def test_purehotcool_component_setup_only_once(devices, login, hass):
async def test_purehotcoollink_component_setup_only_once(devices, login, hass):
"""Test if entities are created only once."""
config = _get_config()
- await async_setup_component(hass, dyson_parent.DOMAIN, config)
+ await async_setup_component(hass, DYSON_DOMAIN, config)
await hass.async_block_till_done()
- entity_ids = hass.states.async_entity_ids("climate")
+ entity_ids = hass.states.async_entity_ids(DOMAIN)
assert len(entity_ids) == 1
state = hass.states.get(entity_ids[0])
assert state.name == "Temp Name"
@@ -440,7 +449,7 @@ async def test_purehotcoollink_component_setup_only_once(devices, login, hass):
async def test_purehotcool_update_state(devices, login, hass):
"""Test state update."""
device = devices.return_value[0]
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
event = {
"msg": "CURRENT-STATE",
@@ -472,12 +481,9 @@ async def test_purehotcool_update_state(devices, login, hass):
},
}
device.state = DysonPureHotCoolV2State(json.dumps(event))
+ update_callback = device.add_message_listener.call_args[0][0]
- for call in device.add_message_listener.call_args_list:
- callback = call[0][0]
- if type(callback.__self__) == dyson.DysonPureHotCoolEntity:
- callback(device.state)
-
+ await hass.async_add_executor_job(update_callback, device.state)
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
attributes = state.attributes
@@ -494,9 +500,9 @@ async def test_purehotcool_update_state(devices, login, hass):
async def test_purehotcool_empty_env_attributes(devices, login, hass):
"""Test empty environmental state update."""
device = devices.return_value[0]
- device.environmental_state.temperature = None
+ device.environmental_state.temperature = 0
device.environmental_state.humidity = None
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
@@ -514,7 +520,7 @@ async def test_purehotcool_fan_state_off(devices, login, hass):
"""Test device fan state off."""
device = devices.return_value[0]
device.state.fan_state = FanState.FAN_OFF.value
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
@@ -532,7 +538,7 @@ async def test_purehotcool_hvac_action_cool(devices, login, hass):
"""Test device HVAC action cool."""
device = devices.return_value[0]
device.state.fan_power = FanPower.POWER_ON.value
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
@@ -551,7 +557,7 @@ async def test_purehotcool_hvac_action_idle(devices, login, hass):
device = devices.return_value[0]
device.state.fan_power = FanPower.POWER_ON.value
device.state.heat_mode = HeatMode.HEAT_ON.value
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
@@ -568,12 +574,12 @@ async def test_purehotcool_hvac_action_idle(devices, login, hass):
async def test_purehotcool_set_temperature(devices, login, hass):
"""Test set temperature."""
device = devices.return_value[0]
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
attributes = state.attributes
- min_temp = attributes["min_temp"]
- max_temp = attributes["max_temp"]
+ min_temp = attributes[ATTR_MIN_TEMP]
+ max_temp = attributes[ATTR_MAX_TEMP]
await hass.services.async_call(
DOMAIN,
@@ -619,7 +625,7 @@ async def test_purehotcool_set_temperature(devices, login, hass):
async def test_purehotcool_set_fan_mode(devices, login, hass):
"""Test set fan mode."""
device = devices.return_value[0]
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
await hass.services.async_call(
@@ -683,7 +689,7 @@ async def test_purehotcool_set_fan_mode(devices, login, hass):
async def test_purehotcool_set_hvac_mode(devices, login, hass):
"""Test set HVAC mode."""
device = devices.return_value[0]
- await async_setup_component(hass, dyson_parent.DOMAIN, _get_config())
+ await async_setup_component(hass, DYSON_DOMAIN, _get_config())
await hass.async_block_till_done()
await hass.services.async_call(
diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py
index 807cf3565edddc..11770e1f133d3a 100644
--- a/tests/components/dyson/test_fan.py
+++ b/tests/components/dyson/test_fan.py
@@ -38,6 +38,10 @@ def __init__(self):
"""Create new Mock Dyson State."""
pass
+ def __repr__(self):
+ """Mock repr because original one fails since constructor not called."""
+ return ""
+
def _get_dyson_purecool_device():
"""Return a valid device as provided by the Dyson web services."""
diff --git a/tests/components/eafm/conftest.py b/tests/components/eafm/conftest.py
index b25c0f4cdba6d8..7233257f2eb3ef 100644
--- a/tests/components/eafm/conftest.py
+++ b/tests/components/eafm/conftest.py
@@ -1,8 +1,9 @@
"""eafm fixtures."""
-from asynctest import patch
import pytest
+from tests.async_mock import patch
+
@pytest.fixture()
def mock_get_stations():
diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py
index 4656e34a34cbc3..cd71767104f1aa 100644
--- a/tests/components/eafm/test_config_flow.py
+++ b/tests/components/eafm/test_config_flow.py
@@ -1,10 +1,11 @@
"""Tests for eafm config flow."""
-from asynctest import patch
import pytest
from voluptuous.error import MultipleInvalid
from homeassistant.components.eafm import const
+from tests.async_mock import patch
+
async def test_flow_no_discovered_stations(hass, mock_get_stations):
"""Test config flow discovers no station."""
diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py
index 6cce7a2bc4bf48..a7ee0403c7cd9d 100644
--- a/tests/components/eafm/test_sensor.py
+++ b/tests/components/eafm/test_sensor.py
@@ -5,6 +5,7 @@
import pytest
from homeassistant import config_entries
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -250,7 +251,7 @@ async def test_reading_is_sampled(hass, mock_get_station):
state = hass.states.get("sensor.my_station_water_level_stage")
assert state.state == "5"
- assert state.attributes["unit_of_measurement"] == "m"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
async def test_multiple_readings_are_sampled(hass, mock_get_station):
@@ -287,11 +288,11 @@ async def test_multiple_readings_are_sampled(hass, mock_get_station):
state = hass.states.get("sensor.my_station_water_level_stage")
assert state.state == "5"
- assert state.attributes["unit_of_measurement"] == "m"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
state = hass.states.get("sensor.my_station_water_level_second_stage")
assert state.state == "4"
- assert state.attributes["unit_of_measurement"] == "m"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
async def test_ignore_no_latest_reading(hass, mock_get_station):
@@ -327,7 +328,7 @@ async def test_ignore_no_latest_reading(hass, mock_get_station):
state = hass.states.get("sensor.my_station_water_level_stage")
assert state.state == "5"
- assert state.attributes["unit_of_measurement"] == "m"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
state = hass.states.get("sensor.my_station_water_level_second_stage")
assert state is None
@@ -357,7 +358,7 @@ async def test_mark_existing_as_unavailable_if_no_latest(hass, mock_get_station)
state = hass.states.get("sensor.my_station_water_level_stage")
assert state.state == "5"
- assert state.attributes["unit_of_measurement"] == "m"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m"
await poll(
{
diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py
index 6b53af5daa87ed..88b7eb7241d35f 100644
--- a/tests/components/ecobee/test_config_flow.py
+++ b/tests/components/ecobee/test_config_flow.py
@@ -26,7 +26,7 @@ async def test_abort_if_already_setup(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_only"
+ assert result["reason"] == "single_instance_allowed"
async def test_user_step_without_user_input(hass):
diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py
index 5f0f2f5fb14ef9..3b1942aee14f6f 100644
--- a/tests/components/elgato/__init__.py
+++ b/tests/components/elgato/__init__.py
@@ -1,7 +1,7 @@
"""Tests for the Elgato Key Light integration."""
from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@@ -18,25 +18,25 @@ async def init_integration(
aioclient_mock.get(
"http://1.2.3.4:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.put(
"http://1.2.3.4:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://1.2.3.4:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://5.6.7.8:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py
index bd65811133a397..c1dfa697041a6b 100644
--- a/tests/components/elgato/test_config_flow.py
+++ b/tests/components/elgato/test_config_flow.py
@@ -5,7 +5,7 @@
from homeassistant.components.elgato import config_flow
from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -44,7 +44,7 @@ async def test_show_zerconf_form(
aioclient_mock.get(
"http://1.2.3.4:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.ElgatoFlowHandler()
@@ -72,7 +72,7 @@ async def test_connection_error(
data={CONF_HOST: "1.2.3.4", CONF_PORT: 9123},
)
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
assert result["step_id"] == "user"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -89,7 +89,7 @@ async def test_zeroconf_connection_error(
data={"host": "1.2.3.4", "port": 9123},
)
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -110,7 +110,7 @@ async def test_zeroconf_confirm_connection_error(
user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 9123}
)
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -122,7 +122,7 @@ async def test_zeroconf_no_data(
flow.hass = hass
result = await flow.async_step_zeroconf()
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -176,7 +176,7 @@ async def test_full_user_flow_implementation(
aioclient_mock.get(
"http://1.2.3.4:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -208,7 +208,7 @@ async def test_full_zeroconf_flow_implementation(
aioclient_mock.get(
"http://1.2.3.4:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.ElgatoFlowHandler()
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index 0f61178107d0c5..576a464c86a6a3 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -38,6 +38,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONTENT_TYPE_JSON,
HTTP_NOT_FOUND,
HTTP_OK,
HTTP_UNAUTHORIZED,
@@ -245,7 +246,7 @@ async def test_discover_lights(hue_client):
result = await hue_client.get("/api/username/lights")
assert result.status == HTTP_OK
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
result_json = await result.json()
@@ -342,7 +343,7 @@ def mock_service_call(call):
no_brightness_result_json = await no_brightness_result.json()
assert no_brightness_result.status == HTTP_OK
- assert "application/json" in no_brightness_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"]
assert len(no_brightness_result_json) == 1
# Verify that SERVICE_TURN_OFF has been called
@@ -384,7 +385,7 @@ def mock_service_call(call):
no_brightness_result_json = await no_brightness_result.json()
assert no_brightness_result.status == HTTP_OK
- assert "application/json" in no_brightness_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"]
assert len(no_brightness_result_json) == 1
# Verify that SERVICE_TURN_ON has been called
@@ -421,7 +422,7 @@ async def test_discover_full_state(hue_client):
result = await hue_client.get(f"/api/{HUE_API_USERNAME}")
assert result.status == HTTP_OK
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
result_json = await result.json()
@@ -471,7 +472,7 @@ async def test_discover_config(hue_client):
result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config")
assert result.status == 200
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
config_json = await result.json()
@@ -508,7 +509,7 @@ async def test_discover_config(hue_client):
result = await hue_client.get("/api/config")
assert result.status == 200
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
config_json = await result.json()
assert "error" not in config_json
@@ -517,7 +518,7 @@ async def test_discover_config(hue_client):
result = await hue_client.get("/api/wronguser/config")
assert result.status == 200
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
config_json = await result.json()
assert "error" not in config_json
@@ -550,7 +551,7 @@ async def test_get_light_state(hass_hue, hue_client):
result = await hue_client.get("/api/username/lights")
assert result.status == HTTP_OK
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
result_json = await result.json()
@@ -667,7 +668,7 @@ async def test_put_light_state(hass, hass_hue, hue_client):
ceiling_result_json = await ceiling_result.json()
assert ceiling_result.status == HTTP_OK
- assert "application/json" in ceiling_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in ceiling_result.headers["content-type"]
assert len(ceiling_result_json) == 1
@@ -857,7 +858,7 @@ async def test_close_cover(hass_hue, hue_client):
)
assert cover_result.status == HTTP_OK
- assert "application/json" in cover_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in cover_result.headers["content-type"]
for _ in range(7):
future = dt_util.utcnow() + timedelta(seconds=1)
@@ -905,7 +906,7 @@ async def test_set_position_cover(hass_hue, hue_client):
)
assert cover_result.status == HTTP_OK
- assert "application/json" in cover_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in cover_result.headers["content-type"]
cover_result_json = await cover_result.json()
@@ -1104,7 +1105,7 @@ async def test_get_empty_groups_state(hue_client):
# pylint: disable=invalid-name
async def perform_put_test_on_ceiling_lights(
- hass_hue, hue_client, content_type="application/json"
+ hass_hue, hue_client, content_type=CONTENT_TYPE_JSON
):
"""Test the setting of a light."""
# Turn the office light off first
@@ -1124,7 +1125,7 @@ async def perform_put_test_on_ceiling_lights(
)
assert office_result.status == HTTP_OK
- assert "application/json" in office_result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in office_result.headers["content-type"]
office_result_json = await office_result.json()
@@ -1143,7 +1144,7 @@ async def perform_get_light_state_by_number(client, entity_number, expected_stat
assert result.status == expected_status
if expected_status == HTTP_OK:
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
return await result.json()
@@ -1164,7 +1165,7 @@ async def perform_put_light_state(
entity_id,
is_on,
brightness=None,
- content_type="application/json",
+ content_type=CONTENT_TYPE_JSON,
hue=None,
saturation=None,
color_temp=None,
diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py
index 8a5556b1222b2b..e68688399e00c1 100644
--- a/tests/components/emulated_hue/test_upnp.py
+++ b/tests/components/emulated_hue/test_upnp.py
@@ -9,7 +9,7 @@
from homeassistant import const, setup
from homeassistant.components import emulated_hue
from homeassistant.components.emulated_hue import upnp
-from homeassistant.const import HTTP_OK
+from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK
from tests.common import get_test_home_assistant, get_test_instance_port
@@ -167,7 +167,7 @@ def test_create_username(self):
)
assert result.status_code == HTTP_OK
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
resp_json = result.json()
success_json = resp_json[0]
@@ -186,7 +186,7 @@ def test_unauthorized_view(self):
)
assert result.status_code == HTTP_OK
- assert "application/json" in result.headers["content-type"]
+ assert CONTENT_TYPE_JSON in result.headers["content-type"]
resp_json = result.json()
assert len(resp_json) == 1
diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py
index 3a835cb0547219..907feb855698a3 100644
--- a/tests/components/flo/conftest.py
+++ b/tests/components/flo/conftest.py
@@ -5,7 +5,7 @@
import pytest
from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONTENT_TYPE_JSON
from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID
@@ -40,7 +40,7 @@ def aioclient_mock_fixture(aioclient_mock):
"timeNow": now,
}
),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
status=200,
)
# Mocks the device for flo.
@@ -48,28 +48,28 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/devices/98765",
text=load_fixture("flo/device_info_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the water consumption for flo.
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/water/consumption",
text=load_fixture("flo/water_consumption_info_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the location info for flo.
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/locations/mmnnoopp",
text=load_fixture("flo/location_info_expand_devices_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the user info for flo.
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/users/12345abcde",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
params={"expand": "locations"},
)
# Mocks the user info for flo.
@@ -77,14 +77,14 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/users/12345abcde",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the valve open call for flo.
aioclient_mock.post(
"https://api-gw.meetflo.com/api/v2/devices/98765",
text=load_fixture("flo/device_info_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
json={"valve": {"target": "open"}},
)
# Mocks the valve close call for flo.
@@ -92,7 +92,7 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/devices/98765",
text=load_fixture("flo/device_info_response_closed.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
json={"valve": {"target": "closed"}},
)
# Mocks the health test call for flo.
@@ -100,14 +100,14 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/devices/98765/healthTest/run",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
# Mocks the health test call for flo.
aioclient_mock.post(
"https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
json={"systemMode": {"target": "home"}},
)
# Mocks the health test call for flo.
@@ -115,7 +115,7 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
json={"systemMode": {"target": "away"}},
)
# Mocks the health test call for flo.
@@ -123,7 +123,7 @@ def aioclient_mock_fixture(aioclient_mock):
"https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode",
text=load_fixture("flo/user_info_expand_locations_response.json"),
status=200,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
json={
"systemMode": {
"target": "sleep",
diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py
index 265f2ae2d3854b..edc9705b7cd010 100644
--- a/tests/components/flo/test_config_flow.py
+++ b/tests/components/flo/test_config_flow.py
@@ -4,6 +4,7 @@
from homeassistant import config_entries, setup
from homeassistant.components.flo.const import DOMAIN
+from homeassistant.const import CONTENT_TYPE_JSON
from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID
@@ -53,7 +54,7 @@ async def test_form_cannot_connect(hass, aioclient_mock):
"timeNow": now,
}
),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
status=400,
)
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py
index d4e650e2b4a8dc..a43fb7aa52feb1 100644
--- a/tests/components/flunearyou/test_config_flow.py
+++ b/tests/components/flunearyou/test_config_flow.py
@@ -37,7 +37,7 @@ async def test_general_error(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
- assert result["errors"] == {"base": "general_error"}
+ assert result["errors"] == {"base": "unknown"}
async def test_show_form(hass):
diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py
index 594b3aff2b2431..be1fcf4c5ee1d1 100644
--- a/tests/components/flux/test_switch.py
+++ b/tests/components/flux/test_switch.py
@@ -20,7 +20,6 @@
async_mock_service,
mock_restore_cache,
)
-from tests.components.switch import common
async def test_valid_config(hass):
@@ -224,8 +223,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- await common.async_turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -278,8 +281,12 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- await common.async_turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -333,8 +340,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- await common.async_turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -389,8 +400,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -444,8 +459,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -501,8 +520,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -559,8 +582,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -621,8 +648,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -683,8 +714,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -744,8 +779,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -805,8 +844,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -863,8 +906,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -920,8 +967,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -993,8 +1044,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -1053,8 +1108,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
@@ -1106,8 +1165,12 @@ def event_date(hass, event, now=None):
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- await common.async_turn_on(hass, "switch.flux")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.flux"},
+ blocking=True,
+ )
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
call = turn_on_calls[-1]
diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py
index 181c963ee4f857..f655e727667228 100644
--- a/tests/components/forked_daapd/test_config_flow.py
+++ b/tests/components/forked_daapd/test_config_flow.py
@@ -16,7 +16,7 @@
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
-from tests.async_mock import patch
+from tests.async_mock import AsyncMock, patch
from tests.common import MockConfigEntry
SAMPLE_CONFIG = {
@@ -69,7 +69,8 @@ async def test_show_form(hass):
async def test_config_flow(hass, config_entry):
"""Test that the user step works."""
with patch(
- "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection"
+ "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection",
+ new=AsyncMock(),
) as mock_test_connection, patch(
"homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request",
autospec=True,
@@ -119,7 +120,8 @@ async def test_zeroconf_updates_title(hass, config_entry):
async def test_config_flow_no_websocket(hass, config_entry):
"""Test config flow setup without websocket enabled on server."""
with patch(
- "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection"
+ "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection",
+ new=AsyncMock(),
) as mock_test_connection:
# test invalid config data
mock_test_connection.return_value = ["websocket_not_enabled"]
diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py
index a4c8a950299a9b..addb1762df0218 100644
--- a/tests/components/freebox/test_config_flow.py
+++ b/tests/components/freebox/test_config_flow.py
@@ -139,7 +139,7 @@ async def test_on_link_failed(hass):
):
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "connection_failed"}
+ assert result["errors"] == {"base": "cannot_connect"}
with patch(
"homeassistant.components.freebox.router.Freepybox.open",
diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py
index 7298ad753d282d..7802ee60e8ce70 100644
--- a/tests/components/frontend/test_init.py
+++ b/tests/components/frontend/test_init.py
@@ -140,7 +140,7 @@ async def test_themes_api(hass, hass_ws_client):
assert msg["result"]["default_theme"] == "safe_mode"
assert msg["result"]["themes"] == {
- "safe_mode": {"primary-color": "#db4437", "accent-color": "#eeee02"}
+ "safe_mode": {"primary-color": "#db4437", "accent-color": "#ffca28"}
}
diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py
index 99ace50e77df3e..8bf1c6abe15579 100644
--- a/tests/components/geo_location/test_trigger.py
+++ b/tests/components/geo_location/test_trigger.py
@@ -2,11 +2,11 @@
import pytest
from homeassistant.components import automation, zone
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
-from tests.components.automation import common
@pytest.fixture
@@ -98,8 +98,12 @@ async def test_if_fires_on_zone_enter(hass, calls):
)
await hass.async_block_till_done()
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.states.async_set(
"geo_location.entity",
diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py
index dd3132b0117b93..21b6830e7f42b9 100644
--- a/tests/components/geofency/test_init.py
+++ b/tests/components/geofency/test_init.py
@@ -6,6 +6,8 @@
from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
HTTP_OK,
HTTP_UNPROCESSABLE_ENTITY,
STATE_HOME,
@@ -316,5 +318,5 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id):
assert state_1 is not state_2
assert STATE_HOME == state_2.state
- assert state_2.attributes["latitude"] == HOME_LATITUDE
- assert state_2.attributes["longitude"] == HOME_LONGITUDE
+ assert state_2.attributes[ATTR_LATITUDE] == HOME_LATITUDE
+ assert state_2.attributes[ATTR_LONGITUDE] == HOME_LONGITUDE
diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py
new file mode 100644
index 00000000000000..0ba2a9127662c4
--- /dev/null
+++ b/tests/components/goalzero/__init__.py
@@ -0,0 +1,35 @@
+"""Tests for the Goal Zero Yeti integration."""
+
+from homeassistant.const import CONF_HOST, CONF_NAME
+
+from tests.async_mock import AsyncMock, patch
+
+HOST = "1.2.3.4"
+NAME = "Yeti"
+
+CONF_DATA = {
+ CONF_HOST: HOST,
+ CONF_NAME: NAME,
+}
+
+CONF_CONFIG_FLOW = {
+ CONF_HOST: HOST,
+ CONF_NAME: NAME,
+}
+
+
+async def _create_mocked_yeti(raise_exception=False):
+ mocked_yeti = AsyncMock()
+ mocked_yeti.get_state = AsyncMock()
+ return mocked_yeti
+
+
+def _patch_init_yeti(mocked_yeti):
+ return patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti)
+
+
+def _patch_config_flow_yeti(mocked_yeti):
+ return patch(
+ "homeassistant.components.goalzero.config_flow.Yeti",
+ return_value=mocked_yeti,
+ )
diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py
new file mode 100644
index 00000000000000..906a84d7882f6e
--- /dev/null
+++ b/tests/components/goalzero/test_config_flow.py
@@ -0,0 +1,115 @@
+"""Test Goal Zero Yeti config flow."""
+from goalzero import exceptions
+
+from homeassistant.components.goalzero.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.data_entry_flow import (
+ RESULT_TYPE_ABORT,
+ RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_FORM,
+)
+
+from . import (
+ CONF_CONFIG_FLOW,
+ CONF_DATA,
+ CONF_HOST,
+ CONF_NAME,
+ NAME,
+ _create_mocked_yeti,
+ _patch_config_flow_yeti,
+)
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+def _flow_next(hass, flow_id):
+ return next(
+ flow
+ for flow in hass.config_entries.flow.async_progress()
+ if flow["flow_id"] == flow_id
+ )
+
+
+def _patch_setup():
+ return patch(
+ "homeassistant.components.goalzero.async_setup_entry",
+ return_value=True,
+ )
+
+
+async def test_flow_user(hass):
+ """Test user initialized flow."""
+ mocked_yeti = await _create_mocked_yeti()
+ with _patch_config_flow_yeti(mocked_yeti), _patch_setup():
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=CONF_CONFIG_FLOW,
+ )
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == NAME
+ assert result["data"] == CONF_DATA
+
+
+async def test_flow_user_already_configured(hass):
+ """Test user initialized flow with duplicate server."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "1.2.3.4", CONF_NAME: "Yeti"},
+ )
+
+ entry.add_to_hass(hass)
+
+ service_info = {
+ "host": "1.2.3.4",
+ "name": "Yeti",
+ }
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=service_info
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_flow_user_cannot_connect(hass):
+ """Test user initialized flow with unreachable server."""
+ mocked_yeti = await _create_mocked_yeti(True)
+ with _patch_config_flow_yeti(mocked_yeti) as yetimock:
+ yetimock.side_effect = exceptions.ConnectError
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_flow_user_invalid_host(hass):
+ """Test user initialized flow with invalid server."""
+ mocked_yeti = await _create_mocked_yeti(True)
+ with _patch_config_flow_yeti(mocked_yeti) as yetimock:
+ yetimock.side_effect = exceptions.InvalidHost
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "invalid_host"}
+
+
+async def test_flow_user_unknown_error(hass):
+ """Test user initialized flow with unreachable server."""
+ mocked_yeti = await _create_mocked_yeti(True)
+ with _patch_config_flow_yeti(mocked_yeti) as yetimock:
+ yetimock.side_effect = Exception
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "unknown"}
diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py
index eb2907e2b6e0fa..91bffca56ce62f 100644
--- a/tests/components/gogogate2/test_cover.py
+++ b/tests/components/gogogate2/test_cover.py
@@ -25,6 +25,7 @@
DEVICE_TYPE_GOGOGATE2,
DEVICE_TYPE_ISMARTGATE,
DOMAIN,
+ MANUFACTURER,
)
from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
from homeassistant.config import async_process_ha_core_config
@@ -49,54 +50,18 @@
from homeassistant.util.dt import utcnow
from tests.async_mock import MagicMock, patch
-from tests.common import MockConfigEntry, async_fire_time_changed
+from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry
-@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
-async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None:
- """Test the failure to import."""
- api = MagicMock(spec=GogoGate2Api)
- api.info.side_effect = ApiError(22, "Error")
- gogogate2api_mock.return_value = api
-
- hass_config = {
- HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC},
- COVER_DOMAIN: [
- {
- CONF_PLATFORM: "gogogate2",
- CONF_NAME: "cover0",
- CONF_DEVICE: DEVICE_TYPE_GOGOGATE2,
- CONF_IP_ADDRESS: "127.0.1.0",
- CONF_USERNAME: "user0",
- CONF_PASSWORD: "password0",
- }
- ],
- }
-
- await async_process_ha_core_config(hass, hass_config[HA_DOMAIN])
- assert await async_setup_component(hass, HA_DOMAIN, {})
- assert await async_setup_component(hass, COVER_DOMAIN, hass_config)
- await hass.async_block_till_done()
-
- entity_ids = hass.states.async_entity_ids(COVER_DOMAIN)
- assert not entity_ids
-
-
-@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
-@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
-async def test_import(
- ismartgateapi_mock, gogogate2api_mock, hass: HomeAssistant
-) -> None:
- """Test importing of file based config."""
- api0 = MagicMock(spec=GogoGate2Api)
- api0.info.return_value = GogoGate2InfoResponse(
+def _mocked_gogogate_open_door_response():
+ return GogoGate2InfoResponse(
user="user1",
gogogatename="gogogatename0",
- model="",
+ model="gogogate2",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc123.blah.blah",
- firmwareversion="",
+ firmwareversion="222",
apicode="",
door1=GogoGate2Door(
door_id=1,
@@ -110,6 +75,7 @@ async def test_import(
camera=False,
events=2,
temperature=None,
+ voltage=40,
),
door2=GogoGate2Door(
door_id=2,
@@ -123,6 +89,7 @@ async def test_import(
camera=False,
events=0,
temperature=None,
+ voltage=40,
),
door3=GogoGate2Door(
door_id=3,
@@ -136,22 +103,23 @@ async def test_import(
camera=False,
events=0,
temperature=None,
+ voltage=40,
),
outputs=Outputs(output1=True, output2=False, output3=True),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
- gogogate2api_mock.return_value = api0
- api1 = MagicMock(spec=ISmartGateApi)
- api1.info.return_value = ISmartGateInfoResponse(
+
+def _mocked_ismartgate_closed_door_response():
+ return ISmartGateInfoResponse(
user="user1",
ismartgatename="ismartgatename0",
- model="",
+ model="ismartgatePRO",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc321.blah.blah",
- firmwareversion="",
+ firmwareversion="555",
pin=123,
lang="en",
newfirmware=False,
@@ -170,11 +138,12 @@ async def test_import(
enabled=True,
apicode="apicode0",
customimage=False,
+ voltage=40,
),
door2=ISmartGateDoor(
- door_id=1,
+ door_id=2,
permission=True,
- name=None,
+ name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
@@ -186,26 +155,72 @@ async def test_import(
enabled=True,
apicode="apicode0",
customimage=False,
+ voltage=40,
),
door3=ISmartGateDoor(
- door_id=1,
+ door_id=3,
permission=True,
name=None,
gate=False,
mode=DoorMode.GARAGE,
- status=DoorStatus.CLOSED,
+ status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
- events=2,
+ events=0,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
+ voltage=40,
),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
+
+
+@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
+async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None:
+ """Test the failure to import."""
+ api = MagicMock(spec=GogoGate2Api)
+ api.info.side_effect = ApiError(22, "Error")
+ gogogate2api_mock.return_value = api
+
+ hass_config = {
+ HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC},
+ COVER_DOMAIN: [
+ {
+ CONF_PLATFORM: "gogogate2",
+ CONF_NAME: "cover0",
+ CONF_DEVICE: DEVICE_TYPE_GOGOGATE2,
+ CONF_IP_ADDRESS: "127.0.1.0",
+ CONF_USERNAME: "user0",
+ CONF_PASSWORD: "password0",
+ }
+ ],
+ }
+
+ await async_process_ha_core_config(hass, hass_config[HA_DOMAIN])
+ assert await async_setup_component(hass, HA_DOMAIN, {})
+ assert await async_setup_component(hass, COVER_DOMAIN, hass_config)
+ await hass.async_block_till_done()
+
+ entity_ids = hass.states.async_entity_ids(COVER_DOMAIN)
+ assert not entity_ids
+
+
+@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
+@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
+async def test_import(
+ ismartgateapi_mock, gogogate2api_mock, hass: HomeAssistant
+) -> None:
+ """Test importing of file based config."""
+ api0 = MagicMock(spec=GogoGate2Api)
+ api0.info.return_value = _mocked_gogogate_open_door_response()
+ gogogate2api_mock.return_value = api0
+
+ api1 = MagicMock(spec=ISmartGateApi)
+ api1.info.return_value = _mocked_ismartgate_closed_door_response()
ismartgateapi_mock.return_value = api1
hass_config = {
@@ -237,13 +252,14 @@ async def test_import(
entity_ids = hass.states.async_entity_ids(COVER_DOMAIN)
assert entity_ids is not None
- assert len(entity_ids) == 2
+ assert len(entity_ids) == 3
assert "cover.door1" in entity_ids
assert "cover.door1_2" in entity_ids
+ assert "cover.door2" in entity_ids
@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
-async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None:
+async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None:
"""Test open and close and data update."""
def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
@@ -268,6 +284,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
camera=False,
events=2,
temperature=None,
+ voltage=40,
),
door2=GogoGate2Door(
door_id=2,
@@ -281,6 +298,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
camera=False,
events=0,
temperature=None,
+ voltage=40,
),
door3=GogoGate2Door(
door_id=3,
@@ -294,6 +312,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
camera=False,
events=0,
temperature=None,
+ voltage=40,
),
outputs=Outputs(output1=True, output2=False, output3=True),
network=Network(ip=""),
@@ -303,7 +322,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
api = MagicMock(GogoGate2Api)
api.activate.return_value = GogoGate2ActivateResponse(result=True)
api.info.return_value = info_response(DoorStatus.OPENED)
- gogogat2api_mock.return_value = api
+ gogogate2api_mock.return_value = api
config_entry = MockConfigEntry(
domain=DOMAIN,
@@ -355,68 +374,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
"""Test availability."""
- closed_door_response = ISmartGateInfoResponse(
- user="user1",
- ismartgatename="ismartgatename0",
- model="",
- apiversion="",
- remoteaccessenabled=False,
- remoteaccess="abc123.blah.blah",
- firmwareversion="",
- pin=123,
- lang="en",
- newfirmware=False,
- door1=ISmartGateDoor(
- door_id=1,
- permission=True,
- name="Door1",
- gate=False,
- mode=DoorMode.GARAGE,
- status=DoorStatus.CLOSED,
- sensor=True,
- sensorid=None,
- camera=False,
- events=2,
- temperature=None,
- enabled=True,
- apicode="apicode0",
- customimage=False,
- ),
- door2=ISmartGateDoor(
- door_id=2,
- permission=True,
- name="Door2",
- gate=True,
- mode=DoorMode.GARAGE,
- status=DoorStatus.CLOSED,
- sensor=True,
- sensorid=None,
- camera=False,
- events=2,
- temperature=None,
- enabled=True,
- apicode="apicode0",
- customimage=False,
- ),
- door3=ISmartGateDoor(
- door_id=3,
- permission=True,
- name=None,
- gate=False,
- mode=DoorMode.GARAGE,
- status=DoorStatus.UNDEFINED,
- sensor=True,
- sensorid=None,
- camera=False,
- events=0,
- temperature=None,
- enabled=True,
- apicode="apicode0",
- customimage=False,
- ),
- network=Network(ip=""),
- wifi=Wifi(SSID="", linkquality="", signal=""),
- )
+ closed_door_response = _mocked_ismartgate_closed_door_response()
api = MagicMock(ISmartGateApi)
api.info.return_value = closed_door_response
@@ -458,3 +416,73 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
await hass.async_block_till_done()
assert hass.states.get("cover.door1").state == STATE_CLOSED
+
+
+@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
+async def test_device_info_ismartgate(ismartgateapi_mock, hass: HomeAssistant) -> None:
+ """Test device info."""
+ device_registry = mock_device_registry(hass)
+
+ closed_door_response = _mocked_ismartgate_closed_door_response()
+
+ api = MagicMock(ISmartGateApi)
+ api.info.return_value = closed_door_response
+ ismartgateapi_mock.return_value = api
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ source=SOURCE_USER,
+ title="mycontroller",
+ unique_id="xyz",
+ data={
+ CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_USERNAME: "admin",
+ CONF_PASSWORD: "password",
+ },
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ device = device_registry.async_get_device({(DOMAIN, "xyz")}, set())
+ assert device
+ assert device.manufacturer == MANUFACTURER
+ assert device.name == "mycontroller"
+ assert device.model == "ismartgatePRO"
+ assert device.sw_version == "555"
+
+
+@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
+async def test_device_info_gogogate2(gogogate2api_mock, hass: HomeAssistant) -> None:
+ """Test device info."""
+ device_registry = mock_device_registry(hass)
+
+ closed_door_response = _mocked_gogogate_open_door_response()
+
+ api = MagicMock(GogoGate2Api)
+ api.info.return_value = closed_door_response
+ gogogate2api_mock.return_value = api
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ source=SOURCE_USER,
+ title="mycontroller",
+ unique_id="xyz",
+ data={
+ CONF_DEVICE: DEVICE_TYPE_GOGOGATE2,
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_USERNAME: "admin",
+ CONF_PASSWORD: "password",
+ },
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ device = device_registry.async_get_device({(DOMAIN, "xyz")}, set())
+ assert device
+ assert device.manufacturer == MANUFACTURER
+ assert device.name == "mycontroller"
+ assert device.model == "gogogate2"
+ assert device.sw_version == "222"
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index 78d403c20386ad..f35415ee9e49fe 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -976,6 +976,7 @@ async def test_trait_execute_adding_query_data(hass):
"states": {
"online": True,
"cameraStreamAccessUrl": "https://example.com/api/streams/bla",
+ "cameraStreamReceiverAppId": "B12CE3CA",
},
}
]
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index ac0db986f42cf5..d415c4f24769a0 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -135,7 +135,8 @@ async def test_camera_stream(hass):
await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {})
assert trt.query_attributes() == {
- "cameraStreamAccessUrl": "https://example.com/api/streams/bla"
+ "cameraStreamAccessUrl": "https://example.com/api/streams/bla",
+ "cameraStreamReceiverAppId": "B12CE3CA",
}
diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py
index efdbc40ee46d9a..2ffe02570c9fa1 100644
--- a/tests/components/group/test_cover.py
+++ b/tests/components/group/test_cover.py
@@ -494,6 +494,7 @@ async def test_is_opening_closing(hass, setup_comp):
await hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True
)
+ await hass.async_block_till_done()
assert hass.states.get(DEMO_COVER_POS).state == STATE_OPENING
assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING
diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py
index 9684c107bb78d8..18d37267334c41 100644
--- a/tests/components/group/test_init.py
+++ b/tests/components/group/test_init.py
@@ -8,12 +8,15 @@
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
+ EVENT_HOMEASSISTANT_START,
+ SERVICE_RELOAD,
STATE_HOME,
STATE_NOT_HOME,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
+from homeassistant.core import CoreState
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
from homeassistant.setup import async_setup_component, setup_component
@@ -29,6 +32,8 @@ class TestComponentsGroup(unittest.TestCase):
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ for domain in ["device_tracker", "light", "group", "sensor"]:
+ setup_component(self.hass, domain, {})
self.addCleanup(self.hass.stop)
def test_setup_group_with_mixed_groupable_states(self):
@@ -62,13 +67,13 @@ def test_setup_group_with_non_groupable_states(self):
self.hass, "chromecasts", ["cast.living_room", "cast.bedroom"]
)
- assert STATE_UNKNOWN == grp.state
+ assert grp.state is None
def test_setup_empty_group(self):
"""Try to set up an empty group."""
grp = group.Group.create_group(self.hass, "nothing", [])
- assert STATE_UNKNOWN == grp.state
+ assert grp.state is None
def test_monitor_group(self):
"""Test if the group keeps track of states."""
@@ -143,22 +148,6 @@ def test_allgroup_turn_on_if_last_turns_on(self):
group_state = self.hass.states.get(test_group.entity_id)
assert STATE_ON == group_state.state
- def test_is_on(self):
- """Test is_on method."""
- self.hass.states.set("light.Bowl", STATE_ON)
- self.hass.states.set("light.Ceiling", STATE_OFF)
- test_group = group.Group.create_group(
- self.hass, "init_group", ["light.Bowl", "light.Ceiling"], False
- )
-
- assert group.is_on(self.hass, test_group.entity_id)
- self.hass.states.set("light.Bowl", STATE_OFF)
- self.hass.block_till_done()
- assert not group.is_on(self.hass, test_group.entity_id)
-
- # Try on non existing state
- assert not group.is_on(self.hass, "non.existing")
-
def test_expand_entity_ids(self):
"""Test expand_entity_ids method."""
self.hass.states.set("light.Bowl", STATE_ON)
@@ -272,42 +261,6 @@ def test_group_being_init_before_first_tracked_state_is_set_to_off(self):
group_state = self.hass.states.get(test_group.entity_id)
assert STATE_OFF == group_state.state
- def test_setup(self):
- """Test setup method."""
- self.hass.states.set("light.Bowl", STATE_ON)
- self.hass.states.set("light.Ceiling", STATE_OFF)
- test_group = group.Group.create_group(
- self.hass, "init_group", ["light.Bowl", "light.Ceiling"], False
- )
-
- group_conf = OrderedDict()
- group_conf["second_group"] = {
- "entities": f"light.Bowl, {test_group.entity_id}",
- "icon": "mdi:work",
- }
- group_conf["test_group"] = "hello.world,sensor.happy"
- group_conf["empty_group"] = {"name": "Empty Group", "entities": None}
-
- setup_component(self.hass, "group", {"group": group_conf})
-
- group_state = self.hass.states.get(f"{group.DOMAIN}.second_group")
- assert STATE_ON == group_state.state
- assert {test_group.entity_id, "light.bowl"} == set(
- group_state.attributes["entity_id"]
- )
- assert group_state.attributes.get(group.ATTR_AUTO) is None
- assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
- assert 1 == group_state.attributes.get(group.ATTR_ORDER)
-
- group_state = self.hass.states.get(f"{group.DOMAIN}.test_group")
- assert STATE_UNKNOWN == group_state.state
- assert {"sensor.happy", "hello.world"} == set(
- group_state.attributes["entity_id"]
- )
- assert group_state.attributes.get(group.ATTR_AUTO) is None
- assert group_state.attributes.get(ATTR_ICON) is None
- assert 2 == group_state.attributes.get(group.ATTR_ORDER)
-
def test_groups_get_unique_names(self):
"""Two groups with same name should both have a unique entity id."""
grp1 = group.Group.create_group(self.hass, "Je suis Charlie")
@@ -367,72 +320,150 @@ def test_group_updated_after_device_tracker_zone_change(self):
self.hass.block_till_done()
assert STATE_NOT_HOME == self.hass.states.get(f"{group.DOMAIN}.peeps").state
- def test_reloading_groups(self):
- """Test reloading the group config."""
- assert setup_component(
- self.hass,
- "group",
- {
- "group": {
- "second_group": {"entities": "light.Bowl", "icon": "mdi:work"},
- "test_group": "hello.world,sensor.happy",
- "empty_group": {"name": "Empty Group", "entities": None},
- }
- },
- )
- group.Group.create_group(
- self.hass, "all tests", ["test.one", "test.two"], user_defined=False
- )
+async def test_is_on(hass):
+ """Test is_on method."""
+ hass.states.async_set("light.Bowl", STATE_ON)
+ hass.states.async_set("light.Ceiling", STATE_OFF)
- assert sorted(self.hass.states.entity_ids()) == [
- "group.all_tests",
- "group.empty_group",
- "group.second_group",
- "group.test_group",
- ]
- assert self.hass.bus.listeners["state_changed"] == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["sensor.happy"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
-
- with patch(
- "homeassistant.config.load_yaml_config_file",
- return_value={
- "group": {"hello": {"entities": "light.Bowl", "icon": "mdi:work"}}
- },
- ):
- common.reload(self.hass)
- self.hass.block_till_done()
-
- assert sorted(self.hass.states.entity_ids()) == [
- "group.all_tests",
- "group.hello",
- ]
- assert self.hass.bus.listeners["state_changed"] == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
- assert len(self.hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
-
- def test_modify_group(self):
- """Test modifying a group."""
- group_conf = OrderedDict()
- group_conf["modify_group"] = {"name": "friendly_name", "icon": "mdi:work"}
-
- assert setup_component(self.hass, "group", {"group": group_conf})
-
- # The old way would create a new group modify_group1 because
- # internally it didn't know anything about those created in the config
- common.set_group(self.hass, "modify_group", icon="mdi:play")
- self.hass.block_till_done()
+ assert group.is_on(hass, "group.none") is False
+ assert await async_setup_component(hass, "light", {})
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
+
+ test_group = await group.Group.async_create_group(
+ hass, "init_group", ["light.Bowl", "light.Ceiling"], False
+ )
+ await hass.async_block_till_done()
+
+ assert group.is_on(hass, test_group.entity_id) is True
+ hass.states.async_set("light.Bowl", STATE_OFF)
+ await hass.async_block_till_done()
+ assert group.is_on(hass, test_group.entity_id) is False
+
+ # Try on non existing state
+ assert not group.is_on(hass, "non.existing")
+
+
+async def test_reloading_groups(hass):
+ """Test reloading the group config."""
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "second_group": {"entities": "light.Bowl", "icon": "mdi:work"},
+ "test_group": "hello.world,sensor.happy",
+ "empty_group": {"name": "Empty Group", "entities": None},
+ }
+ },
+ )
+ await hass.async_block_till_done()
- group_state = self.hass.states.get(f"{group.DOMAIN}.modify_group")
+ await group.Group.async_create_group(
+ hass, "all tests", ["test.one", "test.two"], user_defined=False
+ )
- assert self.hass.states.entity_ids() == ["group.modify_group"]
- assert group_state.attributes.get(ATTR_ICON) == "mdi:play"
- assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name"
+ await hass.async_block_till_done()
+
+ assert sorted(hass.states.async_entity_ids()) == [
+ "group.all_tests",
+ "group.empty_group",
+ "group.second_group",
+ "group.test_group",
+ ]
+ assert hass.bus.async_listeners()["state_changed"] == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["hello.world"]) == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
+
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ return_value={
+ "group": {"hello": {"entities": "light.Bowl", "icon": "mdi:work"}}
+ },
+ ):
+ await hass.services.async_call(group.DOMAIN, SERVICE_RELOAD)
+ await hass.async_block_till_done()
+
+ assert sorted(hass.states.async_entity_ids()) == [
+ "group.all_tests",
+ "group.hello",
+ ]
+ assert hass.bus.async_listeners()["state_changed"] == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["light.bowl"]) == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.one"]) == 1
+ assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS]["test.two"]) == 1
+
+
+async def test_modify_group(hass):
+ """Test modifying a group."""
+ group_conf = OrderedDict()
+ group_conf["modify_group"] = {
+ "name": "friendly_name",
+ "icon": "mdi:work",
+ "entities": None,
+ }
+
+ assert await async_setup_component(hass, "group", {"group": group_conf})
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{group.DOMAIN}.modify_group")
+
+ # The old way would create a new group modify_group1 because
+ # internally it didn't know anything about those created in the config
+ common.async_set_group(hass, "modify_group", icon="mdi:play")
+ await hass.async_block_till_done()
+
+ group_state = hass.states.get(f"{group.DOMAIN}.modify_group")
+ assert group_state
+
+ assert hass.states.async_entity_ids() == ["group.modify_group"]
+ assert group_state.attributes.get(ATTR_ICON) == "mdi:play"
+ assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name"
+
+
+async def test_setup(hass):
+ """Test setup method."""
+ hass.states.async_set("light.Bowl", STATE_ON)
+ hass.states.async_set("light.Ceiling", STATE_OFF)
+
+ group_conf = OrderedDict()
+ group_conf["test_group"] = "hello.world,sensor.happy"
+ group_conf["empty_group"] = {"name": "Empty Group", "entities": None}
+ assert await async_setup_component(hass, "light", {})
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(hass, "group", {"group": group_conf})
+ await hass.async_block_till_done()
+
+ test_group = await group.Group.async_create_group(
+ hass, "init_group", ["light.Bowl", "light.Ceiling"], False
+ )
+ await group.Group.async_create_group(
+ hass,
+ "created_group",
+ ["light.Bowl", f"{test_group.entity_id}"],
+ True,
+ "mdi:work",
+ )
+ await hass.async_block_till_done()
+
+ group_state = hass.states.get(f"{group.DOMAIN}.created_group")
+ assert STATE_ON == group_state.state
+ assert {test_group.entity_id, "light.bowl"} == set(
+ group_state.attributes["entity_id"]
+ )
+ assert group_state.attributes.get(group.ATTR_AUTO) is None
+ assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
+ assert 3 == group_state.attributes.get(group.ATTR_ORDER)
+
+ group_state = hass.states.get(f"{group.DOMAIN}.test_group")
+ assert STATE_UNKNOWN == group_state.state
+ assert {"sensor.happy", "hello.world"} == set(group_state.attributes["entity_id"])
+ assert group_state.attributes.get(group.ATTR_AUTO) is None
+ assert group_state.attributes.get(ATTR_ICON) is None
+ assert 0 == group_state.attributes.get(group.ATTR_ORDER)
async def test_service_group_services(hass):
@@ -496,6 +527,7 @@ async def test_group_order(hass):
"""Test that order gets incremented when creating a new group."""
hass.states.async_set("light.bowl", STATE_ON)
+ assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
@@ -518,6 +550,7 @@ async def test_group_order_with_dynamic_creation(hass):
"""Test that order gets incremented when creating a new group."""
hass.states.async_set("light.bowl", STATE_ON)
+ assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
@@ -563,3 +596,620 @@ async def test_group_order_with_dynamic_creation(hass):
await hass.async_block_till_done()
assert hass.states.get("group.new_group2").attributes["order"] == 4
+
+
+async def test_group_persons(hass):
+ """Test group of persons."""
+ hass.states.async_set("person.one", "Work")
+ hass.states.async_set("person.two", "Work")
+ hass.states.async_set("person.three", "home")
+
+ assert await async_setup_component(hass, "person", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "person.one, person.two, person.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "home"
+
+
+async def test_group_persons_and_device_trackers(hass):
+ """Test group of persons and device_tracker."""
+ hass.states.async_set("person.one", "Work")
+ hass.states.async_set("person.two", "Work")
+ hass.states.async_set("person.three", "Work")
+ hass.states.async_set("device_tracker.one", "home")
+
+ assert await async_setup_component(hass, "person", {})
+ assert await async_setup_component(hass, "device_tracker", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "entities": "device_tracker.one, person.one, person.two, person.three"
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "home"
+
+
+async def test_group_mixed_domains_on(hass):
+ """Test group of mixed domains that is on."""
+ hass.states.async_set("lock.alexander_garage_exit_door", "locked")
+ hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "on")
+ hass.states.async_set("cover.small_garage_door", "open")
+
+ for domain in ["lock", "binary_sensor", "cover"]:
+ assert await async_setup_component(hass, domain, {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "all": "true",
+ "entities": "lock.alexander_garage_exit_door, binary_sensor.alexander_garage_side_door_open, cover.small_garage_door",
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "on"
+
+
+async def test_group_mixed_domains_off(hass):
+ """Test group of mixed domains that is off."""
+ hass.states.async_set("lock.alexander_garage_exit_door", "unlocked")
+ hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "off")
+ hass.states.async_set("cover.small_garage_door", "closed")
+
+ for domain in ["lock", "binary_sensor", "cover"]:
+ assert await async_setup_component(hass, domain, {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "all": "true",
+ "entities": "lock.alexander_garage_exit_door, binary_sensor.alexander_garage_side_door_open, cover.small_garage_door",
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "off"
+
+
+async def test_group_locks(hass):
+ """Test group of locks."""
+ hass.states.async_set("lock.one", "locked")
+ hass.states.async_set("lock.two", "locked")
+ hass.states.async_set("lock.three", "unlocked")
+
+ assert await async_setup_component(hass, "lock", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "lock.one, lock.two, lock.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "locked"
+
+
+async def test_group_sensors(hass):
+ """Test group of sensors."""
+ hass.states.async_set("sensor.one", "locked")
+ hass.states.async_set("sensor.two", "on")
+ hass.states.async_set("sensor.three", "closed")
+
+ assert await async_setup_component(hass, "sensor", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "sensor.one, sensor.two, sensor.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "unknown"
+
+
+async def test_group_climate_mixed(hass):
+ """Test group of climate with mixed states."""
+ hass.states.async_set("climate.one", "off")
+ hass.states.async_set("climate.two", "cool")
+ hass.states.async_set("climate.three", "heat")
+
+ assert await async_setup_component(hass, "climate", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "climate.one, climate.two, climate.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == STATE_ON
+
+
+async def test_group_climate_all_cool(hass):
+ """Test group of climate all set to cool."""
+ hass.states.async_set("climate.one", "cool")
+ hass.states.async_set("climate.two", "cool")
+ hass.states.async_set("climate.three", "cool")
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "climate.one, climate.two, climate.three"},
+ }
+ },
+ )
+ assert await async_setup_component(hass, "climate", {})
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == STATE_ON
+
+
+async def test_group_climate_all_off(hass):
+ """Test group of climate all set to off."""
+ hass.states.async_set("climate.one", "off")
+ hass.states.async_set("climate.two", "off")
+ hass.states.async_set("climate.three", "off")
+
+ assert await async_setup_component(hass, "climate", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "climate.one, climate.two, climate.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == STATE_OFF
+
+
+async def test_group_alarm(hass):
+ """Test group of alarm control panels."""
+ hass.states.async_set("alarm_control_panel.one", "armed_away")
+ hass.states.async_set("alarm_control_panel.two", "armed_home")
+ hass.states.async_set("alarm_control_panel.three", "armed_away")
+ hass.state = CoreState.stopped
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "entities": "alarm_control_panel.one, alarm_control_panel.two, alarm_control_panel.three"
+ },
+ }
+ },
+ )
+ assert await async_setup_component(hass, "alarm_control_panel", {})
+ await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+ assert hass.states.get("group.group_zero").state == STATE_ON
+
+
+async def test_group_alarm_disarmed(hass):
+ """Test group of alarm control panels disarmed."""
+ hass.states.async_set("alarm_control_panel.one", "disarmed")
+ hass.states.async_set("alarm_control_panel.two", "disarmed")
+ hass.states.async_set("alarm_control_panel.three", "disarmed")
+
+ assert await async_setup_component(hass, "alarm_control_panel", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "entities": "alarm_control_panel.one, alarm_control_panel.two, alarm_control_panel.three"
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == STATE_OFF
+
+
+async def test_group_vacuum_off(hass):
+ """Test group of vacuums."""
+ hass.states.async_set("vacuum.one", "docked")
+ hass.states.async_set("vacuum.two", "off")
+ hass.states.async_set("vacuum.three", "off")
+ hass.state = CoreState.stopped
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "vacuum.one, vacuum.two, vacuum.three"},
+ }
+ },
+ )
+ assert await async_setup_component(hass, "vacuum", {})
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+ assert hass.states.get("group.group_zero").state == STATE_OFF
+
+
+async def test_group_vacuum_on(hass):
+ """Test group of vacuums."""
+ hass.states.async_set("vacuum.one", "cleaning")
+ hass.states.async_set("vacuum.two", "off")
+ hass.states.async_set("vacuum.three", "off")
+
+ assert await async_setup_component(hass, "vacuum", {})
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "vacuum.one, vacuum.two, vacuum.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == STATE_ON
+
+
+async def test_device_tracker_not_home(hass):
+ """Test group of device_tracker not_home."""
+ hass.states.async_set("device_tracker.one", "not_home")
+ hass.states.async_set("device_tracker.two", "not_home")
+ hass.states.async_set("device_tracker.three", "not_home")
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {
+ "entities": "device_tracker.one, device_tracker.two, device_tracker.three"
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "not_home"
+
+
+async def test_light_removed(hass):
+ """Test group of lights when one is removed."""
+ hass.states.async_set("light.one", "off")
+ hass.states.async_set("light.two", "off")
+ hass.states.async_set("light.three", "on")
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "light.one, light.two, light.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "on"
+
+ hass.states.async_remove("light.three")
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "off"
+
+
+async def test_switch_removed(hass):
+ """Test group of switches when one is removed."""
+ hass.states.async_set("switch.one", "off")
+ hass.states.async_set("switch.two", "off")
+ hass.states.async_set("switch.three", "on")
+
+ hass.state = CoreState.stopped
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "group_zero": {"entities": "switch.one, switch.two, switch.three"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "unknown"
+ assert await async_setup_component(hass, "switch", {})
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+ assert hass.states.get("group.group_zero").state == "on"
+
+ hass.states.async_remove("switch.three")
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.group_zero").state == "off"
+
+
+async def test_lights_added_after_group(hass):
+ """Test lights added after group."""
+
+ entity_ids = [
+ "light.living_front_ri",
+ "light.living_back_lef",
+ "light.living_back_cen",
+ "light.living_front_le",
+ "light.living_front_ce",
+ "light.living_back_rig",
+ ]
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "living_room_downlights": {"entities": entity_ids},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downlights").state == "unknown"
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "off")
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downlights").state == "off"
+
+
+async def test_lights_added_before_group(hass):
+ """Test lights added before group."""
+
+ entity_ids = [
+ "light.living_front_ri",
+ "light.living_back_lef",
+ "light.living_back_cen",
+ "light.living_front_le",
+ "light.living_front_ce",
+ "light.living_back_rig",
+ ]
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "off")
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "living_room_downlights": {"entities": entity_ids},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downlights").state == "off"
+
+
+async def test_cover_added_after_group(hass):
+ """Test cover added after group."""
+
+ entity_ids = [
+ "cover.upstairs",
+ "cover.downstairs",
+ ]
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "shades": {"entities": entity_ids},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "open")
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.shades").state == "open"
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "closed")
+
+ await hass.async_block_till_done()
+ assert hass.states.get("group.shades").state == "closed"
+
+
+async def test_group_that_references_a_group_of_lights(hass):
+ """Group that references a group of lights."""
+
+ entity_ids = [
+ "light.living_front_ri",
+ "light.living_back_lef",
+ ]
+ hass.state = CoreState.stopped
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "off")
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "living_room_downlights": {"entities": entity_ids},
+ "grouped_group": {
+ "entities": ["group.living_room_downlights", *entity_ids]
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downlights").state == "off"
+ assert hass.states.get("group.grouped_group").state == "off"
+
+
+async def test_group_that_references_a_group_of_covers(hass):
+ """Group that references a group of covers."""
+
+ entity_ids = [
+ "cover.living_front_ri",
+ "cover.living_back_lef",
+ ]
+ hass.state = CoreState.stopped
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "closed")
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "living_room_downcover": {"entities": entity_ids},
+ "grouped_group": {
+ "entities": ["group.living_room_downlights", *entity_ids]
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downcover").state == "closed"
+ assert hass.states.get("group.grouped_group").state == "closed"
+
+
+async def test_group_that_references_two_groups_of_covers(hass):
+ """Group that references a group of covers."""
+
+ entity_ids = [
+ "cover.living_front_ri",
+ "cover.living_back_lef",
+ ]
+ hass.state = CoreState.stopped
+
+ for entity_id in entity_ids:
+ hass.states.async_set(entity_id, "closed")
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "living_room_downcover": {"entities": entity_ids},
+ "living_room_upcover": {"entities": entity_ids},
+ "grouped_group": {
+ "entities": [
+ "group.living_room_downlights",
+ "group.living_room_upcover",
+ ]
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.living_room_downcover").state == "closed"
+ assert hass.states.get("group.living_room_upcover").state == "closed"
+ assert hass.states.get("group.grouped_group").state == "closed"
+
+
+async def test_group_that_references_two_types_of_groups(hass):
+ """Group that references a group of covers and device_trackers."""
+
+ group_1_entity_ids = [
+ "cover.living_front_ri",
+ "cover.living_back_lef",
+ ]
+ group_2_entity_ids = [
+ "device_tracker.living_front_ri",
+ "device_tracker.living_back_lef",
+ ]
+ hass.state = CoreState.stopped
+
+ for entity_id in group_1_entity_ids:
+ hass.states.async_set(entity_id, "closed")
+ for entity_id in group_2_entity_ids:
+ hass.states.async_set(entity_id, "home")
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ "group",
+ {
+ "group": {
+ "covers": {"entities": group_1_entity_ids},
+ "device_trackers": {"entities": group_2_entity_ids},
+ "grouped_group": {
+ "entities": ["group.covers", "group.device_trackers"]
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("group.covers").state == "closed"
+ assert hass.states.get("group.device_trackers").state == "home"
+ assert hass.states.get("group.grouped_group").state == "on"
diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py
index a22c56b4bfc017..ba8fecbed32fee 100644
--- a/tests/components/group/test_light.py
+++ b/tests/components/group/test_light.py
@@ -737,6 +737,11 @@ async def test_reload_with_base_integration_platform_not_setup(hass):
},
)
await hass.async_block_till_done()
+ hass.states.async_set("light.master_hall_lights", STATE_ON)
+ hass.states.async_set("light.master_hall_lights_2", STATE_OFF)
+
+ hass.states.async_set("light.outside_patio_lights", STATE_OFF)
+ hass.states.async_set("light.outside_patio_lights_2", STATE_OFF)
yaml_path = path.join(
_get_fixtures_base_path(),
@@ -755,6 +760,8 @@ async def test_reload_with_base_integration_platform_not_setup(hass):
assert hass.states.get("light.light_group") is None
assert hass.states.get("light.master_hall_lights_g") is not None
assert hass.states.get("light.outside_patio_lights_g") is not None
+ assert hass.states.get("light.master_hall_lights_g").state == STATE_ON
+ assert hass.states.get("light.outside_patio_lights_g").state == STATE_OFF
def _get_fixtures_base_path():
diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py
index a10bee374f87d1..db0cf877d37b55 100644
--- a/tests/components/guardian/test_config_flow.py
+++ b/tests/components/guardian/test_config_flow.py
@@ -138,4 +138,4 @@ async def test_step_zeroconf_no_discovery_info(hass):
DOMAIN, context={"source": SOURCE_ZEROCONF}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
index 7386cf57d0ca6f..d069b311ab2314 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -3,6 +3,8 @@
import pytest
+from homeassistant.components.hassio.http import _need_auth
+
from tests.async_mock import patch
@@ -129,3 +131,32 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo
req_headers = aioclient_mock.mock_calls[0][-1]
req_headers["X-Hass-User-ID"] == hass_admin_user.id
req_headers["X-Hass-Is-Admin"] == "1"
+
+
+async def test_snapshot_upload_headers(hassio_client, aioclient_mock):
+ """Test that we forward the full header for snapshot upload."""
+ content_type = "multipart/form-data; boundary='--webkit'"
+ aioclient_mock.get("http://127.0.0.1/snapshots/new/upload")
+
+ resp = await hassio_client.get(
+ "/api/hassio/snapshots/new/upload", headers={"Content-Type": content_type}
+ )
+
+ # Check we got right response
+ assert resp.status == 200
+
+ assert len(aioclient_mock.mock_calls) == 1
+
+ req_headers = aioclient_mock.mock_calls[0][-1]
+ req_headers["Content-Type"] == content_type
+
+
+def test_need_auth(hass):
+ """Test if the requested path needs authentication."""
+ assert not _need_auth(hass, "addons/test/logo")
+ assert _need_auth(hass, "snapshots/new/upload")
+ assert _need_auth(hass, "supervisor/logs")
+
+ hass.data["onboarding"] = False
+ assert not _need_auth(hass, "snapshots/new/upload")
+ assert not _need_auth(hass, "supervisor/logs")
diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py
index 800814df0130d3..bcc2ed67b2933b 100644
--- a/tests/components/heos/test_config_flow.py
+++ b/tests/components/heos/test_config_flow.py
@@ -20,7 +20,7 @@ async def test_flow_aborts_already_setup(hass, config_entry):
flow.hass = hass
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
async def test_no_host_shows_form(hass):
@@ -41,7 +41,7 @@ async def test_cannot_connect_shows_error_form(hass, controller):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- assert result["errors"][CONF_HOST] == "connection_failure"
+ assert result["errors"][CONF_HOST] == "cannot_connect"
assert controller.connect.call_count == 1
assert controller.disconnect.call_count == 1
controller.connect.reset_mock()
@@ -118,7 +118,7 @@ async def test_discovery_flow_aborts_already_setup(
flow.hass = hass
result = await flow.async_step_ssdp(discovery_data)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
async def test_discovery_sets_the_unique_id(hass, controller, discovery_data):
diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py
index b2d96707d7e854..eba0eb0f3fb746 100644
--- a/tests/components/heos/test_media_player.py
+++ b/tests/components/heos/test_media_player.py
@@ -120,6 +120,7 @@ async def test_updates_from_signals(hass, config_entry, config, controller, favo
const.SIGNAL_PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
+
state = hass.states.get("media_player.test_player")
assert state.state == STATE_PLAYING
@@ -227,6 +228,7 @@ async def set_signal():
const.SIGNAL_CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, change_data
)
await event.wait()
+ await hass.async_block_till_done()
assert hass.states.get("media_player.test_player").state == STATE_PLAYING
diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py
index c15e4431f8721c..494ea89b3b8a90 100644
--- a/tests/components/history/test_init.py
+++ b/tests/components/history/test_init.py
@@ -18,7 +18,7 @@
init_recorder_component,
mock_state_change_event,
)
-from tests.components.recorder.common import wait_recording_done
+from tests.components.recorder.common import trigger_db_commit, wait_recording_done
class TestComponentHistory(unittest.TestCase):
@@ -823,3 +823,150 @@ async def test_fetch_period_api_with_include_order(hass, hass_client):
params={"filter_entity_id": "non.existing,something.else"},
)
assert response.status == 200
+
+
+async def test_fetch_period_api_with_entity_glob_include(hass, hass_client):
+ """Test the fetch period view for history."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(
+ hass,
+ "history",
+ {
+ "history": {
+ "include": {"entity_globs": ["light.k*"]},
+ }
+ },
+ )
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ hass.states.async_set("light.kitchen", "on")
+ hass.states.async_set("light.cow", "on")
+ hass.states.async_set("light.nomatch", "on")
+
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ client = await hass_client()
+ response = await client.get(
+ f"/api/history/period/{dt_util.utcnow().isoformat()}",
+ )
+ assert response.status == 200
+ response_json = await response.json()
+ assert response_json[0][0]["entity_id"] == "light.kitchen"
+
+
+async def test_fetch_period_api_with_entity_glob_exclude(hass, hass_client):
+ """Test the fetch period view for history."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(
+ hass,
+ "history",
+ {
+ "history": {
+ "exclude": {
+ "entity_globs": ["light.k*"],
+ "domains": "switch",
+ "entities": "media_player.test",
+ },
+ }
+ },
+ )
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ hass.states.async_set("light.kitchen", "on")
+ hass.states.async_set("light.cow", "on")
+ hass.states.async_set("light.match", "on")
+ hass.states.async_set("switch.match", "on")
+ hass.states.async_set("media_player.test", "on")
+
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ client = await hass_client()
+ response = await client.get(
+ f"/api/history/period/{dt_util.utcnow().isoformat()}",
+ )
+ assert response.status == 200
+ response_json = await response.json()
+ assert len(response_json) == 2
+ assert response_json[0][0]["entity_id"] == "light.cow"
+ assert response_json[1][0]["entity_id"] == "light.match"
+
+
+async def test_fetch_period_api_with_entity_glob_include_and_exclude(hass, hass_client):
+ """Test the fetch period view for history."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(
+ hass,
+ "history",
+ {
+ "history": {
+ "exclude": {
+ "entity_globs": ["light.many*"],
+ },
+ "include": {
+ "entity_globs": ["light.m*"],
+ "domains": "switch",
+ "entities": "media_player.test",
+ },
+ }
+ },
+ )
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ hass.states.async_set("light.kitchen", "on")
+ hass.states.async_set("light.cow", "on")
+ hass.states.async_set("light.match", "on")
+ hass.states.async_set("light.many_state_changes", "on")
+ hass.states.async_set("switch.match", "on")
+ hass.states.async_set("media_player.test", "on")
+
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ client = await hass_client()
+ response = await client.get(
+ f"/api/history/period/{dt_util.utcnow().isoformat()}",
+ )
+ assert response.status == 200
+ response_json = await response.json()
+ assert len(response_json) == 3
+ assert response_json[0][0]["entity_id"] == "light.match"
+ assert response_json[1][0]["entity_id"] == "media_player.test"
+ assert response_json[2][0]["entity_id"] == "switch.match"
+
+
+async def test_entity_ids_limit_via_api(hass, hass_client):
+ """Test limiting history to entity_ids."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(
+ hass,
+ "history",
+ {"history": {}},
+ )
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ hass.states.async_set("light.kitchen", "on")
+ hass.states.async_set("light.cow", "on")
+ hass.states.async_set("light.nomatch", "on")
+
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ client = await hass_client()
+ response = await client.get(
+ f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow",
+ )
+ assert response.status == 200
+ response_json = await response.json()
+ assert len(response_json) == 2
+ assert response_json[0][0]["entity_id"] == "light.kitchen"
+ assert response_json[1][0]["entity_id"] == "light.cow"
diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py
index a1f502a3475449..8f47d891f9fd81 100644
--- a/tests/components/homeassistant/test_scene.py
+++ b/tests/components/homeassistant/test_scene.py
@@ -320,3 +320,12 @@ async def test_config(hass):
no_icon = hass.states.get("scene.scene_no_icon")
assert no_icon is not None
assert "icon" not in no_icon.attributes
+
+
+def test_validator():
+ """Test validators."""
+ parsed = ha_scene.STATES_SCHEMA({"light.Test": {"state": "on"}})
+ assert len(parsed) == 1
+ assert "light.test" in parsed
+ assert parsed["light.test"].entity_id == "light.test"
+ assert parsed["light.test"].state == "on"
diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py
index 84b7e725f0acbf..aa0acae254c9e6 100644
--- a/tests/components/homeassistant/triggers/test_event.py
+++ b/tests/components/homeassistant/triggers/test_event.py
@@ -2,11 +2,11 @@
import pytest
import homeassistant.components.automation as automation
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
-from tests.components.automation import common
@pytest.fixture
@@ -15,6 +15,12 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
+@pytest.fixture
+def context_with_user():
+ """Track calls to a mock service."""
+ return Context(user_id="test_user_id")
+
+
@pytest.fixture(autouse=True)
def setup_comp(hass):
"""Initialize components."""
@@ -38,19 +44,23 @@ async def test_if_fires_on_event(hass, calls):
hass.bus.async_fire("test_event", context=context)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
assert calls[0].context.parent_id == context.id
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
-async def test_if_fires_on_event_extra_data(hass, calls):
- """Test the firing of events still matches with event data."""
+async def test_if_fires_on_event_extra_data(hass, calls, context_with_user):
+ """Test the firing of events still matches with event data and context."""
assert await async_setup_component(
hass,
automation.DOMAIN,
@@ -61,21 +71,26 @@ async def test_if_fires_on_event_extra_data(hass, calls):
}
},
)
-
- hass.bus.async_fire("test_event", {"extra_key": "extra_data"})
+ hass.bus.async_fire(
+ "test_event", {"extra_key": "extra_data"}, context=context_with_user
+ )
await hass.async_block_till_done()
assert len(calls) == 1
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
-async def test_if_fires_on_event_with_data(hass, calls):
- """Test the firing of events with data."""
+async def test_if_fires_on_event_with_data_and_context(hass, calls, context_with_user):
+ """Test the firing of events with data and context."""
assert await async_setup_component(
hass,
automation.DOMAIN,
@@ -88,6 +103,7 @@ async def test_if_fires_on_event_with_data(hass, calls):
"some_attr": "some_value",
"second_attr": "second_value",
},
+ "context": {"user_id": context_with_user.user_id},
},
"action": {"service": "test.automation"},
}
@@ -97,17 +113,31 @@ async def test_if_fires_on_event_with_data(hass, calls):
hass.bus.async_fire(
"test_event",
{"some_attr": "some_value", "another": "value", "second_attr": "second_value"},
+ context=context_with_user,
)
await hass.async_block_till_done()
assert len(calls) == 1
- hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"})
+ hass.bus.async_fire(
+ "test_event",
+ {"some_attr": "some_value", "another": "value"},
+ context=context_with_user,
+ )
await hass.async_block_till_done()
assert len(calls) == 1 # No new call
+ hass.bus.async_fire(
+ "test_event",
+ {"some_attr": "some_value", "another": "value", "second_attr": "second_value"},
+ )
+ await hass.async_block_till_done()
+ assert len(calls) == 1
-async def test_if_fires_on_event_with_empty_data_config(hass, calls):
- """Test the firing of events with empty data config.
+
+async def test_if_fires_on_event_with_empty_data_and_context_config(
+ hass, calls, context_with_user
+):
+ """Test the firing of events with empty data and context config.
The frontend automation editor can produce configurations with an
empty dict for event_data instead of no key.
@@ -121,13 +151,18 @@ async def test_if_fires_on_event_with_empty_data_config(hass, calls):
"platform": "event",
"event_type": "test_event",
"event_data": {},
+ "context": {},
},
"action": {"service": "test.automation"},
}
},
)
- hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"})
+ hass.bus.async_fire(
+ "test_event",
+ {"some_attr": "some_value", "another": "value"},
+ context=context_with_user,
+ )
await hass.async_block_till_done()
assert len(calls) == 1
@@ -157,7 +192,7 @@ async def test_if_fires_on_event_with_nested_data(hass, calls):
async def test_if_not_fires_if_event_data_not_matches(hass, calls):
- """Test firing of event if no match."""
+ """Test firing of event if no data match."""
assert await async_setup_component(
hass,
automation.DOMAIN,
@@ -176,3 +211,27 @@ async def test_if_not_fires_if_event_data_not_matches(hass, calls):
hass.bus.async_fire("test_event", {"some_attr": "some_other_value"})
await hass.async_block_till_done()
assert len(calls) == 0
+
+
+async def test_if_not_fires_if_event_context_not_matches(
+ hass, calls, context_with_user
+):
+ """Test firing of event if no context match."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event",
+ "context": {"user_id": "some_user"},
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event", {}, context=context_with_user)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py
index 932dde91120b91..09a13f956035a8 100644
--- a/tests/components/homeassistant/triggers/test_numeric_state.py
+++ b/tests/components/homeassistant/triggers/test_numeric_state.py
@@ -8,6 +8,7 @@
from homeassistant.components.homeassistant.triggers import (
numeric_state as numeric_state_trigger,
)
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -19,7 +20,6 @@
async_mock_service,
mock_component,
)
-from tests.components.automation import common
@pytest.fixture
@@ -59,8 +59,13 @@ async def test_if_fires_on_entity_change_below(hass, calls):
# Set above 12 so the automation will fire again
hass.states.async_set("test.entity", 12)
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.states.async_set("test.entity", 9)
await hass.async_block_till_done()
assert len(calls) == 1
@@ -863,9 +868,12 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls):
hass.states.async_set("test.entity_1", 9)
hass.states.async_set("test.entity_2", 9)
await hass.async_block_till_done()
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
-
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py
index ce9ecaba1b0a5b..61fa991e0f4f85 100644
--- a/tests/components/homeassistant/triggers/test_state.py
+++ b/tests/components/homeassistant/triggers/test_state.py
@@ -5,6 +5,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.homeassistant.triggers import state as state_trigger
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -16,7 +17,6 @@
async_mock_service,
mock_component,
)
-from tests.components.automation import common
@pytest.fixture
@@ -70,8 +70,12 @@ async def test_if_fires_on_entity_change(hass, calls):
assert calls[0].context.parent_id == context.id
assert calls[0].data["some"] == "state - test.entity - hello - world - None"
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.states.async_set("test.entity", "planet")
await hass.async_block_till_done()
assert len(calls) == 1
@@ -394,8 +398,12 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls):
hass.states.async_set("test.entity_1", "world")
hass.states.async_set("test.entity_2", "world")
await hass.async_block_till_done()
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
@@ -538,6 +546,39 @@ async def test_if_fires_on_entity_change_with_for_without_to(hass, calls):
assert len(calls) == 1
+async def test_if_does_not_fires_on_entity_change_with_for_without_to_2(hass, calls):
+ """Test for firing on entity change with for."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "state",
+ "entity_id": "test.entity",
+ "for": {"seconds": 5},
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = utcnow
+
+ for i in range(10):
+ hass.states.async_set("test.entity", str(i))
+ await hass.async_block_till_done()
+
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 0
+
+
async def test_if_fires_on_entity_creation_and_removal(hass, calls):
"""Test for firing on entity creation and removal, with to/from constraints."""
# set automations for multiple combinations to/from
@@ -1199,3 +1240,32 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop(
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
+
+
+async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean(
+ hass, calls
+):
+ """Test for firing if both filters are match attribute."""
+ hass.states.async_set("test.entity", "bla", {"happening": False})
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "state",
+ "entity_id": "test.entity",
+ "from": False,
+ "to": True,
+ "attribute": "happening",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.states.async_set("test.entity", "bla", {"happening": True})
+ await hass.async_block_till_done()
+ assert len(calls) == 1
diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py
index 0ef071aadb60c6..f428bcf29bc5cf 100644
--- a/tests/components/homeassistant/triggers/test_time_pattern.py
+++ b/tests/components/homeassistant/triggers/test_time_pattern.py
@@ -1,15 +1,17 @@
"""The tests for the time_pattern automation."""
+from datetime import timedelta
+
import pytest
import voluptuous as vol
import homeassistant.components.automation as automation
import homeassistant.components.homeassistant.triggers.time_pattern as time_pattern
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.async_mock import patch
from tests.common import async_fire_time_changed, async_mock_service, mock_component
-from tests.components.automation import common
@pytest.fixture
@@ -53,8 +55,12 @@ async def test_if_fires_when_hour_matches(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_time_changed(hass, now.replace(year=now.year + 1, hour=0))
await hass.async_block_till_done()
@@ -123,6 +129,39 @@ async def test_if_fires_when_second_matches(hass, calls):
assert len(calls) == 1
+async def test_if_fires_when_second_as_string_matches(hass, calls):
+ """Test for firing if seconds are matching."""
+ now = dt_util.utcnow()
+ time_that_will_not_match_right_away = dt_util.utcnow().replace(
+ year=now.year + 1, second=15
+ )
+ with patch(
+ "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
+ ):
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "time_pattern",
+ "hours": "*",
+ "minutes": "*",
+ "seconds": "30",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ async_fire_time_changed(
+ hass, time_that_will_not_match_right_away + timedelta(seconds=15)
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
async def test_if_fires_when_all_matches(hass, calls):
"""Test for firing if everything matches."""
now = dt_util.utcnow()
diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py
index 79c15344b17642..0cb31e1b7012bc 100644
--- a/tests/components/homekit/conftest.py
+++ b/tests/components/homekit/conftest.py
@@ -29,10 +29,3 @@ def events(hass):
EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
)
yield events
-
-
-@pytest.fixture
-def mock_zeroconf():
- """Mock zeroconf."""
- with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
- yield mock_zc.return_value
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index bcbbbf3bcbf473..ea91733fdab030 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -24,6 +24,7 @@
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_TYPE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -119,6 +120,12 @@ def test_types(type_name, entity_id, state, attrs, config):
ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE,
},
),
+ (
+ "Window",
+ "cover.set_position",
+ "open",
+ {ATTR_DEVICE_CLASS: "window", ATTR_SUPPORTED_FEATURES: 4},
+ ),
("WindowCovering", "cover.set_position", "open", {ATTR_SUPPORTED_FEATURES: 4}),
(
"WindowCoveringBasic",
@@ -190,7 +197,7 @@ def test_type_media_player(type_name, entity_id, state, attrs, config):
),
("LightSensor", "sensor.light", "900", {ATTR_DEVICE_CLASS: "illuminance"}),
("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: "lm"}),
- ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: "lx"}),
+ ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}),
(
"TemperatureSensor",
"sensor.temperature",
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index 757281af1e9c10..c22d6286e76ee3 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -1018,6 +1018,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
options={},
)
+ assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}})
system_zc = await zeroconf.async_get_instance(hass)
with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch(
diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py
index 3eed6d05816669..cd193b61646da4 100644
--- a/tests/components/homekit/test_type_covers.py
+++ b/tests/components/homekit/test_type_covers.py
@@ -48,10 +48,13 @@ def cls():
"homeassistant.components.homekit.type_covers",
fromlist=["GarageDoorOpener", "WindowCovering", "WindowCoveringBasic"],
)
- patcher_tuple = namedtuple("Cls", ["window", "window_basic", "garage"])
+ patcher_tuple = namedtuple(
+ "Cls", ["window", "windowcovering", "windowcovering_basic", "garage"]
+ )
yield patcher_tuple(
- window=_import.WindowCovering,
- window_basic=_import.WindowCoveringBasic,
+ window=_import.Window,
+ windowcovering=_import.WindowCovering,
+ windowcovering_basic=_import.WindowCoveringBasic,
garage=_import.GarageDoorOpener,
)
patcher.stop()
@@ -136,13 +139,13 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_window_set_cover_position(hass, hk_driver, cls, events):
+async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
- acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
+ acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@@ -206,7 +209,24 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
-async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
+async def test_window_instantiate(hass, hk_driver, cls, events):
+ """Test if Window accessory is instantiated correctly."""
+ entity_id = "cover.window"
+
+ hass.states.async_set(entity_id, None)
+ await hass.async_block_till_done()
+ acc = cls.window(hass, hk_driver, "Window", entity_id, 2, None)
+ await acc.run_handler()
+ await hass.async_block_till_done()
+
+ assert acc.aid == 2
+ assert acc.category == 13 # Window
+
+ assert acc.char_current_position.value == 0
+ assert acc.char_target_position.value == 0
+
+
+async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events):
"""Test if accessory and HA update slat tilt accordingly."""
entity_id = "cover.window"
@@ -214,7 +234,7 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION}
)
await hass.async_block_till_done()
- acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
+ acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@@ -273,12 +293,12 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
-async def test_window_open_close(hass, hk_driver, cls, events):
+async def test_windowcovering_open_close(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0})
- acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None)
+ acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@@ -354,14 +374,14 @@ async def test_window_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_window_open_close_stop(hass, hk_driver, cls, events):
+async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}
)
- acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None)
+ acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@@ -401,7 +421,9 @@ async def test_window_open_close_stop(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, events):
+async def test_windowcovering_open_close_with_position_and_stop(
+ hass, hk_driver, cls, events
+):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.stop_window"
@@ -410,7 +432,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev
STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION},
)
- acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
+ acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@@ -430,7 +452,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev
assert events[-1].data[ATTR_VALUE] is None
-async def test_window_basic_restore(hass, hk_driver, cls, events):
+async def test_windowcovering_basic_restore(hass, hk_driver, cls, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
@@ -455,20 +477,22 @@ async def test_window_basic_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.window_basic(hass, hk_driver, "Cover", "cover.simple", 2, None)
+ acc = cls.windowcovering_basic(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
- acc = cls.window_basic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
+ acc = cls.windowcovering_basic(
+ hass, hk_driver, "Cover", "cover.all_info_set", 2, None
+ )
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
-async def test_window_restore(hass, hk_driver, cls, events):
+async def test_windowcovering_restore(hass, hk_driver, cls, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
@@ -493,13 +517,13 @@ async def test_window_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.window(hass, hk_driver, "Cover", "cover.simple", 2, None)
+ acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
- acc = cls.window(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
+ acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py
index 20029861adbdcc..e82bc5bb15ddb6 100644
--- a/tests/components/homekit/test_type_lights.py
+++ b/tests/components/homekit/test_type_lights.py
@@ -293,9 +293,25 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
+ assert acc.char_hue.value == 260
+ assert acc.char_saturation.value == 90
assert not hasattr(acc, "char_color_temperature")
+ hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224})
+ await hass.async_block_till_done()
+ await acc.run_handler()
+ await hass.async_block_till_done()
+ assert acc.char_hue.value == 27
+ assert acc.char_saturation.value == 27
+
+ hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352})
+ await hass.async_block_till_done()
+ await acc.run_handler()
+ await hass.async_block_till_done()
+ assert acc.char_hue.value == 28
+ assert acc.char_saturation.value == 61
+
async def test_light_rgb_color(hass, hk_driver, cls, events):
"""Test light with rgb_color."""
diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index b139fac36577ee..d6bf74bb7cfa17 100644
--- a/tests/components/homekit/test_type_security_systems.py
+++ b/tests/components/homekit/test_type_security_systems.py
@@ -1,7 +1,14 @@
"""Test different accessory types: Security Systems."""
+from pyhap.loader import get_loader
import pytest
from homeassistant.components.alarm_control_panel import DOMAIN
+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.components.homekit.const import ATTR_VALUE
from homeassistant.components.homekit.type_security_systems import SecuritySystem
from homeassistant.const import (
@@ -129,3 +136,116 @@ async def test_no_alarm_code(hass, hk_driver, config, events):
assert acc.char_target_state.value == 0
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None
+
+
+async def test_supported_states(hass, hk_driver, events):
+ """Test different supported states."""
+ code = "1234"
+ config = {ATTR_CODE: code}
+ entity_id = "alarm_control_panel.test"
+
+ loader = get_loader()
+ default_current_states = loader.get_char(
+ "SecuritySystemCurrentState"
+ ).properties.get("ValidValues")
+ default_target_services = loader.get_char(
+ "SecuritySystemTargetState"
+ ).properties.get("ValidValues")
+
+ # Set up a number of test configuration
+ test_configs = [
+ {
+ "features": SUPPORT_ALARM_ARM_HOME,
+ "current_values": [
+ default_current_states["Disarmed"],
+ default_current_states["AlarmTriggered"],
+ default_current_states["StayArm"],
+ ],
+ "target_values": [
+ default_target_services["Disarm"],
+ default_target_services["StayArm"],
+ ],
+ },
+ {
+ "features": SUPPORT_ALARM_ARM_AWAY,
+ "current_values": [
+ default_current_states["Disarmed"],
+ default_current_states["AlarmTriggered"],
+ default_current_states["AwayArm"],
+ ],
+ "target_values": [
+ default_target_services["Disarm"],
+ default_target_services["AwayArm"],
+ ],
+ },
+ {
+ "features": SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY,
+ "current_values": [
+ default_current_states["Disarmed"],
+ default_current_states["AlarmTriggered"],
+ default_current_states["StayArm"],
+ default_current_states["AwayArm"],
+ ],
+ "target_values": [
+ default_target_services["Disarm"],
+ default_target_services["StayArm"],
+ default_target_services["AwayArm"],
+ ],
+ },
+ {
+ "features": SUPPORT_ALARM_ARM_HOME
+ | SUPPORT_ALARM_ARM_AWAY
+ | SUPPORT_ALARM_ARM_NIGHT,
+ "current_values": [
+ default_current_states["Disarmed"],
+ default_current_states["AlarmTriggered"],
+ default_current_states["StayArm"],
+ default_current_states["AwayArm"],
+ default_current_states["NightArm"],
+ ],
+ "target_values": [
+ default_target_services["Disarm"],
+ default_target_services["StayArm"],
+ default_target_services["AwayArm"],
+ default_target_services["NightArm"],
+ ],
+ },
+ {
+ "features": SUPPORT_ALARM_ARM_HOME
+ | SUPPORT_ALARM_ARM_AWAY
+ | SUPPORT_ALARM_ARM_NIGHT
+ | SUPPORT_ALARM_TRIGGER,
+ "current_values": [
+ default_current_states["Disarmed"],
+ default_current_states["AlarmTriggered"],
+ default_current_states["StayArm"],
+ default_current_states["AwayArm"],
+ default_current_states["NightArm"],
+ ],
+ "target_values": [
+ default_target_services["Disarm"],
+ default_target_services["StayArm"],
+ default_target_services["AwayArm"],
+ default_target_services["NightArm"],
+ ],
+ },
+ ]
+
+ for test_config in test_configs:
+ attrs = {"supported_features": test_config.get("features")}
+
+ hass.states.async_set(entity_id, None, attributes=attrs)
+ await hass.async_block_till_done()
+
+ acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config)
+ await acc.run_handler()
+ await hass.async_block_till_done()
+
+ valid_current_values = acc.char_current_state.properties.get("ValidValues")
+ valid_target_values = acc.char_target_state.properties.get("ValidValues")
+
+ for val in valid_current_values.values():
+ assert val in test_config.get("current_values")
+
+ for val in valid_target_values.values():
+ assert val in test_config.get("target_values")
diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
index 292c416968800a..8bee6e0591d9ec 100644
--- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
+++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
@@ -1,7 +1,7 @@
"""
Regression tests for Aqara Gateway V3.
-https://github.com/home-assistant/home-assistant/issues/20957
+https://github.com/home-assistant/core/issues/20957
"""
from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR
diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
index ee048f93ca78fd..c1a956f3f4dcbb 100644
--- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py
+++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
@@ -1,7 +1,7 @@
"""
Regression tests for Ecobee 3.
-https://github.com/home-assistant/home-assistant/issues/15336
+https://github.com/home-assistant/core/issues/15336
"""
from unittest import mock
diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
index b1a8c0a636fab4..6823cdc16ead81 100644
--- a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
+++ b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
@@ -1,7 +1,7 @@
"""
Regression tests for Ecobee occupancy.
-https://github.com/home-assistant/home-assistant/issues/31827
+https://github.com/home-assistant/core/issues/31827
"""
from tests.components.homekit_controller.common import (
diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
index 2abd12b3df4ccd..4682d4b2bcce84 100644
--- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
+++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
@@ -59,7 +59,7 @@ async def test_recover_from_failure(hass, utcnow, failure_cls):
"""
Test that entity actually recovers from a network connection drop.
- See https://github.com/home-assistant/home-assistant/issues/18949
+ See https://github.com/home-assistant/core/issues/18949
"""
accessories = await setup_accessories_from_file(hass, "koogeek_ls1.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
index 3209139ae1e631..fe7b0c7783f785 100644
--- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
+++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
@@ -1,7 +1,7 @@
"""
Regression tests for Aqara Gateway V3.
-https://github.com/home-assistant/home-assistant/issues/20885
+https://github.com/home-assistant/core/issues/20885
"""
from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE
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 a83a31166f45ed..b30dd1e9c89d65 100644
--- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py
+++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py
@@ -1,7 +1,7 @@
"""
Test against characteristics captured from a SIMPLEconnect Fan.
-https://github.com/home-assistant/home-assistant/issues/26180
+https://github.com/home-assistant/core/issues/26180
"""
from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index 09c823ff4980e9..a8eb869abf42bc 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -8,10 +8,11 @@
import pytest
from homeassistant.components.homekit_controller import config_flow
+from homeassistant.helpers import device_registry
import tests.async_mock
from tests.async_mock import patch
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, mock_device_registry
PAIRING_START_FORM_ERRORS = [
(KeyError, "pairing_failed"),
@@ -51,14 +52,17 @@
"111-11-111 ",
"111-11-111a",
"1111111",
+ "22222222",
]
VALID_PAIRING_CODES = [
- "111-11-111",
- "123-45-678",
- "11111111",
+ "114-11-111",
+ "123-45-679",
+ "123-45-679 ",
+ "11121111",
"98765432",
+ " 98765432 ",
]
@@ -233,11 +237,45 @@ async def test_pair_already_paired_1(hass, controller):
assert result["reason"] == "already_paired"
+async def test_id_missing(hass, controller):
+ """Test id is missing."""
+ device = setup_mock_accessory(controller)
+ discovery_info = get_device_discovery_info(device)
+
+ # Remove id from device
+ del discovery_info["properties"]["id"]
+
+ # Device is discovered
+ result = await hass.config_entries.flow.async_init(
+ "homekit_controller", context={"source": "zeroconf"}, data=discovery_info
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "invalid_properties"
+
+
async def test_discovery_ignored_model(hass, controller):
"""Already paired."""
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
- discovery_info["properties"]["md"] = config_flow.HOMEKIT_IGNORE[0]
+
+ config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={})
+ formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF")
+
+ dev_reg = mock_device_registry(hass)
+ dev_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={
+ (
+ config_flow.HOMEKIT_BRIDGE_DOMAIN,
+ config_entry.entry_id,
+ config_flow.HOMEKIT_BRIDGE_SERIAL_NUMBER,
+ )
+ },
+ connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)},
+ model=config_flow.HOMEKIT_BRIDGE_MODEL,
+ )
+
+ discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF"
# Device is discovered
result = await hass.config_entries.flow.async_init(
@@ -513,6 +551,7 @@ async def test_user_works(hass, controller):
assert get_flow_context(hass, result) == {
"source": "user",
"unique_id": "00:00:00:00:00:00",
+ "title_placeholders": {"name": "TestDevice"},
}
result = await hass.config_entries.flow.async_configure(
diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py
index f15b2b56a95522..6d6ba84e24388e 100644
--- a/tests/components/homematicip_cloud/test_binary_sensor.py
+++ b/tests/components/homematicip_cloud/test_binary_sensor.py
@@ -38,6 +38,29 @@ async def test_manually_configured_platform(hass):
assert not hass.data.get(HMIPC_DOMAIN)
+async def test_hmip_access_point_cloud_connection_sensor(
+ hass, default_mock_hap_factory
+):
+ """Test HomematicipCloudConnectionSensor."""
+ entity_id = "binary_sensor.access_point_cloud_connection"
+ entity_name = "Access Point Cloud Connection"
+ device_model = None
+ mock_hap = await default_mock_hap_factory.async_get_mock_hap(
+ test_devices=[entity_name]
+ )
+
+ ha_state, hmip_device = get_and_check_entity_basics(
+ hass, mock_hap, entity_id, entity_name, device_model
+ )
+
+ assert ha_state.state == STATE_ON
+
+ await async_manipulate_test_data(hass, hmip_device, "connected", False)
+
+ ha_state = hass.states.get(entity_id)
+ assert ha_state.state == STATE_OFF
+
+
async def test_hmip_acceleration_sensor(hass, default_mock_hap_factory):
"""Test HomematicipAccelerationSensor."""
entity_id = "binary_sensor.garagentor"
diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py
index f7999b5f015355..c47f0bf25eabc6 100644
--- a/tests/components/homematicip_cloud/test_device.py
+++ b/tests/components/homematicip_cloud/test_device.py
@@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory):
test_devices=None, test_groups=None
)
- assert len(mock_hap.hmip_device_by_entity_id) == 191
+ assert len(mock_hap.hmip_device_by_entity_id) == 192
async def test_hmip_remove_device(hass, default_mock_hap_factory):
diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py
index fe7283b471eb6e..20c5c41a5b538d 100644
--- a/tests/components/homematicip_cloud/test_sensor.py
+++ b/tests/components/homematicip_cloud/test_sensor.py
@@ -24,6 +24,8 @@
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
SPEED_KILOMETERS_PER_HOUR,
@@ -44,8 +46,8 @@ async def test_manually_configured_platform(hass):
async def test_hmip_accesspoint_status(hass, default_mock_hap_factory):
"""Test HomematicipSwitch."""
- entity_id = "sensor.access_point"
- entity_name = "Access Point"
+ entity_id = "sensor.access_point_duty_cycle"
+ entity_name = "Access Point Duty Cycle"
device_model = None
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=[entity_name]
@@ -247,7 +249,7 @@ async def test_hmip_illuminance_sensor1(hass, default_mock_hap_factory):
)
assert ha_state.state == "4890.0"
- assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LIGHT_LUX
await async_manipulate_test_data(hass, hmip_device, "illumination", 231)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "231"
@@ -267,7 +269,7 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap_factory):
)
assert ha_state.state == "807.3"
- assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LIGHT_LUX
await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "231"
@@ -337,7 +339,7 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory):
)
assert ha_state.state == "3.9"
- assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm"
+ assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_MILLIMETERS
await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "14.2"
diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py
index ae1e81847271dd..2547aa3f01f694 100644
--- a/tests/components/huawei_lte/test_config_flow.py
+++ b/tests/components/huawei_lte/test_config_flow.py
@@ -17,6 +17,7 @@
CONF_USERNAME,
)
+from tests.async_mock import patch
from tests.common import MockConfigEntry
FIXTURE_USER_INPUT = {
@@ -84,7 +85,7 @@ async def test_connection_error(hass, requests_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- assert result["errors"] == {CONF_URL: "unknown_connection_error"}
+ assert result["errors"] == {CONF_URL: "unknown"}
@pytest.fixture
@@ -111,7 +112,7 @@ def login_requests_mock(requests_mock):
(LoginErrorEnum.PASSWORD_WRONG, {CONF_PASSWORD: "incorrect_password"}),
(
LoginErrorEnum.USERNAME_PWD_WRONG,
- {CONF_USERNAME: "incorrect_username_or_password"},
+ {CONF_USERNAME: "invalid_auth"},
),
(LoginErrorEnum.USERNAME_PWD_ORERRUN, {"base": "login_attempts_exceeded"}),
(ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}),
@@ -140,9 +141,15 @@ async def test_success(hass, login_requests_mock):
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
text="OK",
)
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT
- )
+ with patch("homeassistant.components.huawei_lte.async_setup"), patch(
+ "homeassistant.components.huawei_lte.async_setup_entry"
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data=FIXTURE_USER_INPUT,
+ )
+ await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL]
diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py
index f9834349750339..92bb8cbac5e09a 100644
--- a/tests/components/huawei_lte/test_sensor.py
+++ b/tests/components/huawei_lte/test_sensor.py
@@ -3,11 +3,19 @@
import pytest
from homeassistant.components.huawei_lte import sensor
+from homeassistant.const import (
+ SIGNAL_STRENGTH_DECIBELS,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+)
@pytest.mark.parametrize(
("value", "expected"),
- (("-71 dBm", (-71, "dBm")), ("15dB", (15, "dB")), (">=-51dBm", (-51, "dBm"))),
+ (
+ ("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
+ ("15dB", (15, SIGNAL_STRENGTH_DECIBELS)),
+ (">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
+ ),
)
def test_format_default(value, expected):
"""Test that default formatter copes with expected values."""
diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py
index 1e37ff2e02190e..bd3e955d2d8747 100644
--- a/tests/components/hvv_departures/test_config_flow.py
+++ b/tests/components/hvv_departures/test_config_flow.py
@@ -32,8 +32,11 @@ async def test_user_flow(hass):
with patch(
"homeassistant.components.hvv_departures.hub.GTI.init",
return_value=FIXTURE_INIT,
- ), patch("pygti.gti.GTI.checkName", return_value=FIXTURE_CHECK_NAME,), patch(
- "pygti.gti.GTI.stationInformation",
+ ), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.checkName",
+ return_value=FIXTURE_CHECK_NAME,
+ ), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.stationInformation",
return_value=FIXTURE_STATION_INFORMATION,
), patch(
"homeassistant.components.hvv_departures.async_setup", return_value=True
@@ -96,7 +99,7 @@ async def test_user_flow_no_results(hass):
"homeassistant.components.hvv_departures.hub.GTI.init",
return_value=FIXTURE_INIT,
), patch(
- "pygti.gti.GTI.checkName",
+ "homeassistant.components.hvv_departures.hub.GTI.checkName",
return_value={"returnCode": "OK", "results": []},
), patch(
"homeassistant.components.hvv_departures.async_setup", return_value=True
@@ -186,7 +189,7 @@ async def test_user_flow_station(hass):
"homeassistant.components.hvv_departures.hub.GTI.init",
return_value=True,
), patch(
- "pygti.gti.GTI.checkName",
+ "homeassistant.components.hvv_departures.hub.GTI.checkName",
return_value={"returnCode": "OK", "results": []},
):
@@ -220,7 +223,7 @@ async def test_user_flow_station_select(hass):
"homeassistant.components.hvv_departures.hub.GTI.init",
return_value=True,
), patch(
- "pygti.gti.GTI.checkName",
+ "homeassistant.components.hvv_departures.hub.GTI.checkName",
return_value=FIXTURE_CHECK_NAME,
):
result_user = await hass.config_entries.flow.async_init(
@@ -264,14 +267,15 @@ async def test_options_flow(hass):
)
config_entry.add_to_hass(hass)
- with patch(
+ with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch(
"homeassistant.components.hvv_departures.hub.GTI.init",
return_value=True,
), patch(
- "pygti.gti.GTI.departureList",
+ "homeassistant.components.hvv_departures.hub.GTI.departureList",
return_value=FIXTURE_DEPARTURE_LIST,
):
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)
@@ -314,15 +318,23 @@ async def test_options_flow_invalid_auth(hass):
)
config_entry.add_to_hass(hass)
+ with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True
+ ), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.departureList",
+ return_value=FIXTURE_DEPARTURE_LIST,
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
with patch(
- "homeassistant.components.hvv_departures.hub.GTI.init",
+ "homeassistant.components.hvv_departures.hub.GTI.departureList",
side_effect=InvalidAuth(
"ERROR_TEXT",
"Bei der Verarbeitung der Anfrage ist ein technisches Problem aufgetreten.",
"Authentication failed!",
),
):
- assert await hass.config_entries.async_setup(config_entry.entry_id)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -347,12 +359,19 @@ async def test_options_flow_cannot_connect(hass):
)
config_entry.add_to_hass(hass)
- with patch(
- "pygti.gti.GTI.departureList",
- side_effect=CannotConnect(),
+ with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True
+ ), patch(
+ "homeassistant.components.hvv_departures.hub.GTI.departureList",
+ return_value=FIXTURE_DEPARTURE_LIST,
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.hvv_departures.hub.GTI.departureList",
+ side_effect=CannotConnect(),
+ ):
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py
new file mode 100644
index 00000000000000..e4c1ee67efa702
--- /dev/null
+++ b/tests/components/hyperion/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Hyperion component."""
diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py
new file mode 100644
index 00000000000000..8250cc6c9c2068
--- /dev/null
+++ b/tests/components/hyperion/test_light.py
@@ -0,0 +1,430 @@
+"""Tests for the Hyperion integration."""
+from hyperion import const
+
+from homeassistant.components.hyperion import light as hyperion_light
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_EFFECT,
+ ATTR_HS_COLOR,
+ DOMAIN,
+)
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import AsyncMock, Mock, call, patch
+
+TEST_HOST = "test-hyperion-host"
+TEST_PORT = const.DEFAULT_PORT
+TEST_NAME = "test_hyperion_name"
+TEST_PRIORITY = 128
+TEST_ENTITY_ID = f"{DOMAIN}.{TEST_NAME}"
+
+
+def create_mock_client():
+ """Create a mock Hyperion client."""
+ mock_client = Mock()
+ mock_client.async_client_connect = AsyncMock(return_value=True)
+ mock_client.adjustment = None
+ mock_client.effects = None
+ mock_client.id = "%s:%i" % (TEST_HOST, TEST_PORT)
+ return mock_client
+
+
+def call_registered_callback(client, key, *args, **kwargs):
+ """Call a Hyperion entity callback that was registered with the client."""
+ return client.set_callbacks.call_args[0][0][key](*args, **kwargs)
+
+
+async def setup_entity(hass, client=None):
+ """Add a test Hyperion entity to hass."""
+ client = client or create_mock_client()
+ with patch("hyperion.client.HyperionClient", return_value=client):
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: {
+ "platform": "hyperion",
+ "name": TEST_NAME,
+ "host": TEST_HOST,
+ "port": const.DEFAULT_PORT,
+ "priority": TEST_PRIORITY,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+
+async def test_setup_platform(hass):
+ """Test setting up the platform."""
+ client = create_mock_client()
+ await setup_entity(hass, client=client)
+ assert hass.states.get(TEST_ENTITY_ID) is not None
+
+
+async def test_setup_platform_not_ready(hass):
+ """Test the platform not being ready."""
+ client = create_mock_client()
+ client.async_client_connect = AsyncMock(return_value=False)
+
+ await setup_entity(hass, client=client)
+ assert hass.states.get(TEST_ENTITY_ID) is None
+
+
+async def test_light_basic_properies(hass):
+ """Test the basic properties."""
+ client = create_mock_client()
+ await setup_entity(hass, client=client)
+
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "on"
+ assert entity_state.attributes["brightness"] == 255
+ assert entity_state.attributes["hs_color"] == (0.0, 0.0)
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
+ assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
+
+ # By default the effect list is the 3 external sources + 'Solid'.
+ assert len(entity_state.attributes["effect_list"]) == 4
+
+ assert (
+ entity_state.attributes["supported_features"] == hyperion_light.SUPPORT_HYPERION
+ )
+
+
+async def test_light_async_turn_on(hass):
+ """Test turning the light on."""
+ client = create_mock_client()
+ await setup_entity(hass, client=client)
+
+ # On (=), 100% (=), solid (=), [255,255,255] (=)
+ client.async_send_set_color = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ )
+
+ assert client.async_send_set_color.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_COLOR: [255, 255, 255],
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+
+ # On (=), 50% (!), solid (=), [255,255,255] (=)
+ # ===
+ brightness = 128
+ client.async_send_set_color = AsyncMock(return_value=True)
+ client.async_send_set_adjustment = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
+ blocking=True,
+ )
+
+ assert client.async_send_set_adjustment.call_args == call(
+ **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 50}}
+ )
+ assert client.async_send_set_color.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_COLOR: [255, 255, 255],
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+
+ # Simulate a state callback from Hyperion.
+ client.adjustment = [{const.KEY_BRIGHTNESS: 50}]
+ call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "on"
+ assert entity_state.attributes["brightness"] == brightness
+
+ # On (=), 50% (=), solid (=), [0,255,255] (!)
+ hs_color = (180.0, 100.0)
+ client.async_send_set_color = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HS_COLOR: hs_color},
+ blocking=True,
+ )
+
+ assert client.async_send_set_color.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_COLOR: (0, 255, 255),
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+
+ # Simulate a state callback from Hyperion.
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
+ const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
+ }
+
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["hs_color"] == hs_color
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
+
+ # On (=), 100% (!), solid, [0,255,255] (=)
+ brightness = 255
+ client.async_send_set_color = AsyncMock(return_value=True)
+ client.async_send_set_adjustment = AsyncMock(return_value=True)
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
+ blocking=True,
+ )
+
+ assert client.async_send_set_adjustment.call_args == call(
+ **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 100}}
+ )
+ assert client.async_send_set_color.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_COLOR: (0, 255, 255),
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+ client.adjustment = [{const.KEY_BRIGHTNESS: 100}]
+ call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["brightness"] == brightness
+
+ # On (=), 100% (=), V4L (!), [0,255,255] (=)
+ effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L
+ client.async_send_clear = AsyncMock(return_value=True)
+ client.async_send_set_component = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
+ blocking=True,
+ )
+
+ assert client.async_send_clear.call_args == call(
+ **{const.KEY_PRIORITY: TEST_PRIORITY}
+ )
+ assert client.async_send_set_component.call_args_list == [
+ call(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[0],
+ const.KEY_STATE: False,
+ }
+ }
+ ),
+ call(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[1],
+ const.KEY_STATE: False,
+ }
+ }
+ ),
+ call(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[2],
+ const.KEY_STATE: True,
+ }
+ }
+ ),
+ ]
+ client.visible_priority = {const.KEY_COMPONENTID: effect}
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
+ assert entity_state.attributes["effect"] == effect
+
+ # On (=), 100% (=), "Warm Blobs" (!), [0,255,255] (=)
+ effect = "Warm Blobs"
+ client.async_send_clear = AsyncMock(return_value=True)
+ client.async_send_set_effect = AsyncMock(return_value=True)
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
+ blocking=True,
+ )
+
+ assert client.async_send_clear.call_args == call(
+ **{const.KEY_PRIORITY: TEST_PRIORITY}
+ )
+ assert client.async_send_set_effect.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_EFFECT: {const.KEY_NAME: effect},
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
+ const.KEY_OWNER: effect,
+ }
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
+ assert entity_state.attributes["effect"] == effect
+
+ # No calls if disconnected.
+ client.has_loaded_state = False
+ call_registered_callback(client, "client-update", {"loaded-state": False})
+ client.async_send_clear = AsyncMock(return_value=True)
+ client.async_send_set_effect = AsyncMock(return_value=True)
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ )
+
+ assert not client.async_send_clear.called
+ assert not client.async_send_set_effect.called
+
+
+async def test_light_async_turn_off(hass):
+ """Test turning the light off."""
+ client = create_mock_client()
+ await setup_entity(hass, client=client)
+
+ client.async_send_set_component = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ )
+
+ assert client.async_send_set_component.call_args == call(
+ **{
+ const.KEY_COMPONENTSTATE: {
+ const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
+ const.KEY_STATE: False,
+ }
+ }
+ )
+
+ # No calls if no state loaded.
+ client.has_loaded_state = False
+ client.async_send_set_component = AsyncMock(return_value=True)
+ call_registered_callback(client, "client-update", {"loaded-state": False})
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ )
+
+ assert not client.async_send_set_component.called
+
+
+async def test_light_async_updates_from_hyperion_client(hass):
+ """Test receiving a variety of Hyperion client callbacks."""
+ client = create_mock_client()
+ await setup_entity(hass, client=client)
+
+ # Bright change gets accepted.
+ brightness = 10
+ client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
+ call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
+
+ # Broken brightness value is ignored.
+ bad_brightness = -200
+ client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}]
+ call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
+
+ # Update components.
+ client.is_on.return_value = True
+ call_registered_callback(client, "components-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "on"
+
+ client.is_on.return_value = False
+ call_registered_callback(client, "components-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "off"
+
+ # Update priorities (V4L)
+ client.is_on.return_value = True
+ client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
+ assert entity_state.attributes["hs_color"] == (0.0, 0.0)
+ assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L
+
+ # Update priorities (Effect)
+ effect = "foo"
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
+ const.KEY_OWNER: effect,
+ }
+
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["effect"] == effect
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
+ assert entity_state.attributes["hs_color"] == (0.0, 0.0)
+
+ # Update priorities (Color)
+ rgb = (0, 100, 100)
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
+ const.KEY_VALUE: {const.KEY_RGB: rgb},
+ }
+
+ call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
+ assert entity_state.attributes["hs_color"] == (180.0, 100.0)
+
+ # Update effect list
+ effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
+ client.effects = effects
+ call_registered_callback(client, "effects-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.attributes["effect_list"] == [
+ effect[const.KEY_NAME] for effect in effects
+ ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID]
+
+ # Update connection status (e.g. disconnection).
+
+ # Turn on late, check state, disconnect, ensure it cannot be turned off.
+ client.has_loaded_state = False
+ call_registered_callback(client, "client-update", {"loaded-state": False})
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "unavailable"
+
+ # Update connection status (e.g. re-connection)
+ client.has_loaded_state = True
+ call_registered_callback(client, "client-update", {"loaded-state": True})
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+ assert entity_state.state == "on"
+
+
+async def test_full_state_loaded_on_start(hass):
+ """Test receiving a variety of Hyperion client callbacks."""
+ client = create_mock_client()
+
+ # Update full state (should call all update methods).
+ brightness = 25
+ client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
+ const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)},
+ }
+ client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
+
+ await setup_entity(hass, client=client)
+
+ entity_state = hass.states.get(TEST_ENTITY_ID)
+
+ assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
+ assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
+ assert entity_state.attributes["hs_color"] == (180.0, 100.0)
diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py
index d2fa4633d8076b..38dd2ec1a3aa64 100644
--- a/tests/components/iaqualink/test_config_flow.py
+++ b/tests/components/iaqualink/test_config_flow.py
@@ -58,7 +58,7 @@ async def test_with_invalid_credentials(hass, step):
assert result["type"] == "form"
assert result["step_id"] == "user"
- assert result["errors"] == {"base": "connection_failure"}
+ assert result["errors"] == {"base": "cannot_connect"}
@pytest.mark.parametrize("step", ["import", "user"])
diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py
index 3123ead4eeb6e5..4f70083a14a402 100644
--- a/tests/components/icloud/test_config_flow.py
+++ b/tests/components/icloud/test_config_flow.py
@@ -275,7 +275,7 @@ async def test_login_failed(hass: HomeAssistantType):
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_USERNAME: "login"}
+ assert result["errors"] == {CONF_USERNAME: "invalid_auth"}
async def test_no_device(
diff --git a/tests/components/imap_email_content/test_sensor.py b/tests/components/imap_email_content/test_sensor.py
index 3a0c006d15f074..241dc4dc5064fe 100644
--- a/tests/components/imap_email_content/test_sensor.py
+++ b/tests/components/imap_email_content/test_sensor.py
@@ -4,14 +4,11 @@
import email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
-import unittest
from homeassistant.components.imap_email_content import sensor as imap_email_content
-from homeassistant.helpers.event import track_state_change
+from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.template import Template
-from tests.common import get_test_home_assistant
-
class FakeEMailReader:
"""A test class for sending test emails."""
@@ -31,207 +28,203 @@ def read_next(self):
return self._messages.popleft()
-class EmailContentSensor(unittest.TestCase):
- """Test the IMAP email content sensor."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- def test_allowed_sender(self):
- """Test emails from allowed sender."""
- test_message = email.message.Message()
- test_message["From"] = "sender@test.com"
- test_message["Subject"] = "Test"
- test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
- test_message.set_payload("Test Message")
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([test_message])),
- "test_emails_sensor",
- ["sender@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert "Test" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
- assert "sender@test.com" == sensor.device_state_attributes["from"]
- assert "Test" == sensor.device_state_attributes["subject"]
- assert (
- datetime.datetime(2016, 1, 1, 12, 44, 57)
- == sensor.device_state_attributes["date"]
- )
-
- def test_multi_part_with_text(self):
- """Test multi part emails."""
- msg = MIMEMultipart("alternative")
- msg["Subject"] = "Link"
- msg["From"] = "sender@test.com"
-
- text = "Test Message"
- html = "Test Message"
-
- textPart = MIMEText(text, "plain")
- htmlPart = MIMEText(html, "html")
-
- msg.attach(textPart)
- msg.attach(htmlPart)
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([msg])),
- "test_emails_sensor",
- ["sender@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert "Link" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
-
- def test_multi_part_only_html(self):
- """Test multi part emails with only HTML."""
- msg = MIMEMultipart("alternative")
- msg["Subject"] = "Link"
- msg["From"] = "sender@test.com"
-
- html = "Test Message"
-
- htmlPart = MIMEText(html, "html")
-
- msg.attach(htmlPart)
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([msg])),
- "test_emails_sensor",
- ["sender@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert "Link" == sensor.state
- assert (
- "Test Message"
- == sensor.device_state_attributes["body"]
- )
-
- def test_multi_part_only_other_text(self):
- """Test multi part emails with only other text."""
- msg = MIMEMultipart("alternative")
- msg["Subject"] = "Link"
- msg["From"] = "sender@test.com"
-
- other = "Test Message"
-
- htmlPart = MIMEText(other, "other")
-
- msg.attach(htmlPart)
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([msg])),
- "test_emails_sensor",
- ["sender@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert "Link" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
-
- def test_multiple_emails(self):
- """Test multiple emails."""
- states = []
-
- test_message1 = email.message.Message()
- test_message1["From"] = "sender@test.com"
- test_message1["Subject"] = "Test"
- test_message1["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
- test_message1.set_payload("Test Message")
-
- test_message2 = email.message.Message()
- test_message2["From"] = "sender@test.com"
- test_message2["Subject"] = "Test 2"
- test_message2["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
- test_message2.set_payload("Test Message 2")
-
- def state_changed_listener(entity_id, from_s, to_s):
- states.append(to_s)
-
- track_state_change(self.hass, ["sensor.emailtest"], state_changed_listener)
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([test_message1, test_message2])),
- "test_emails_sensor",
- ["sender@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
-
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
-
- assert "Test" == states[0].state
- assert "Test 2" == states[1].state
-
- assert "Test Message 2" == sensor.device_state_attributes["body"]
-
- def test_sender_not_allowed(self):
- """Test not whitelisted emails."""
- test_message = email.message.Message()
- test_message["From"] = "sender@test.com"
- test_message["Subject"] = "Test"
- test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
- test_message.set_payload("Test Message")
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([test_message])),
- "test_emails_sensor",
- ["other@test.com"],
- None,
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert sensor.state is None
-
- def test_template(self):
- """Test value template."""
- test_message = email.message.Message()
- test_message["From"] = "sender@test.com"
- test_message["Subject"] = "Test"
- test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
- test_message.set_payload("Test Message")
-
- sensor = imap_email_content.EmailContentSensor(
- self.hass,
- FakeEMailReader(deque([test_message])),
- "test_emails_sensor",
- ["sender@test.com"],
- Template(
- "{{ subject }} from {{ from }} with message {{ body }}", self.hass
- ),
- )
-
- sensor.entity_id = "sensor.emailtest"
- sensor.schedule_update_ha_state(True)
- self.hass.block_till_done()
- assert "Test from sender@test.com with message Test Message" == sensor.state
+async def test_allowed_sender(hass):
+ """Test emails from allowed sender."""
+ test_message = email.message.Message()
+ test_message["From"] = "sender@test.com"
+ test_message["Subject"] = "Test"
+ test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
+ test_message.set_payload("Test Message")
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([test_message])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert "Test" == sensor.state
+ assert "Test Message" == sensor.device_state_attributes["body"]
+ assert "sender@test.com" == sensor.device_state_attributes["from"]
+ assert "Test" == sensor.device_state_attributes["subject"]
+ assert (
+ datetime.datetime(2016, 1, 1, 12, 44, 57)
+ == sensor.device_state_attributes["date"]
+ )
+
+
+async def test_multi_part_with_text(hass):
+ """Test multi part emails."""
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = "Link"
+ msg["From"] = "sender@test.com"
+
+ text = "Test Message"
+ html = "Test Message"
+
+ textPart = MIMEText(text, "plain")
+ htmlPart = MIMEText(html, "html")
+
+ msg.attach(textPart)
+ msg.attach(htmlPart)
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([msg])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert "Link" == sensor.state
+ assert "Test Message" == sensor.device_state_attributes["body"]
+
+
+async def test_multi_part_only_html(hass):
+ """Test multi part emails with only HTML."""
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = "Link"
+ msg["From"] = "sender@test.com"
+
+ html = "Test Message"
+
+ htmlPart = MIMEText(html, "html")
+
+ msg.attach(htmlPart)
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([msg])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert "Link" == sensor.state
+ assert (
+ "Test Message"
+ == sensor.device_state_attributes["body"]
+ )
+
+
+async def test_multi_part_only_other_text(hass):
+ """Test multi part emails with only other text."""
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = "Link"
+ msg["From"] = "sender@test.com"
+
+ other = "Test Message"
+
+ htmlPart = MIMEText(other, "other")
+
+ msg.attach(htmlPart)
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([msg])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert "Link" == sensor.state
+ assert "Test Message" == sensor.device_state_attributes["body"]
+
+
+async def test_multiple_emails(hass):
+ """Test multiple emails."""
+ states = []
+
+ test_message1 = email.message.Message()
+ test_message1["From"] = "sender@test.com"
+ test_message1["Subject"] = "Test"
+ test_message1["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
+ test_message1.set_payload("Test Message")
+
+ test_message2 = email.message.Message()
+ test_message2["From"] = "sender@test.com"
+ test_message2["Subject"] = "Test 2"
+ test_message2["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
+ test_message2.set_payload("Test Message 2")
+
+ def state_changed_listener(entity_id, from_s, to_s):
+ states.append(to_s)
+
+ async_track_state_change(hass, ["sensor.emailtest"], state_changed_listener)
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([test_message1, test_message2])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+
+ assert "Test" == states[0].state
+ assert "Test 2" == states[1].state
+
+ assert "Test Message 2" == sensor.device_state_attributes["body"]
+
+
+async def test_sender_not_allowed(hass):
+ """Test not whitelisted emails."""
+ test_message = email.message.Message()
+ test_message["From"] = "sender@test.com"
+ test_message["Subject"] = "Test"
+ test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
+ test_message.set_payload("Test Message")
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([test_message])),
+ "test_emails_sensor",
+ ["other@test.com"],
+ None,
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert sensor.state is None
+
+
+async def test_template(hass):
+ """Test value template."""
+ test_message = email.message.Message()
+ test_message["From"] = "sender@test.com"
+ test_message["Subject"] = "Test"
+ test_message["Date"] = datetime.datetime(2016, 1, 1, 12, 44, 57)
+ test_message.set_payload("Test Message")
+
+ sensor = imap_email_content.EmailContentSensor(
+ hass,
+ FakeEMailReader(deque([test_message])),
+ "test_emails_sensor",
+ ["sender@test.com"],
+ Template("{{ subject }} from {{ from }} with message {{ body }}", hass),
+ )
+
+ sensor.entity_id = "sensor.emailtest"
+ sensor.async_schedule_update_ha_state(True)
+ await hass.async_block_till_done()
+ assert "Test from sender@test.com with message Test Message" == sensor.state
diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py
index ca4e56ff54deb9..edb85e7b98da7f 100644
--- a/tests/components/influxdb/test_init.py
+++ b/tests/components/influxdb/test_init.py
@@ -62,9 +62,11 @@ def mock_client_fixture(request):
def get_mock_call_fixture(request):
"""Get version specific lambda to make write API call mock."""
if request.param == influxdb.API_VERSION_2:
- return lambda body: call(bucket=DEFAULT_BUCKET, record=body)
+ return lambda body, precision=None: call(
+ bucket=DEFAULT_BUCKET, record=body, write_precision=precision
+ )
# pylint: disable=unnecessary-lambda
- return lambda body: call(body)
+ return lambda body, precision=None: call(body, time_precision=precision)
def _get_write_api_mock_v1(mock_influx_client):
@@ -1474,3 +1476,104 @@ async def test_invalid_inputs_error(
== 1
)
sleep.assert_not_called()
+
+
+@pytest.mark.parametrize(
+ "mock_client, config_ext, get_write_api, get_mock_call, precision",
+ [
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ _get_write_api_mock_v1,
+ influxdb.DEFAULT_API_VERSION,
+ "ns",
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ _get_write_api_mock_v2,
+ influxdb.API_VERSION_2,
+ "ns",
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ _get_write_api_mock_v1,
+ influxdb.DEFAULT_API_VERSION,
+ "us",
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ _get_write_api_mock_v2,
+ influxdb.API_VERSION_2,
+ "us",
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ _get_write_api_mock_v1,
+ influxdb.DEFAULT_API_VERSION,
+ "ms",
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ _get_write_api_mock_v2,
+ influxdb.API_VERSION_2,
+ "ms",
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ _get_write_api_mock_v1,
+ influxdb.DEFAULT_API_VERSION,
+ "s",
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ _get_write_api_mock_v2,
+ influxdb.API_VERSION_2,
+ "s",
+ ),
+ ],
+ indirect=["mock_client", "get_mock_call"],
+)
+async def test_precision(
+ hass, mock_client, config_ext, get_write_api, get_mock_call, precision
+):
+ """Test the precision setup."""
+ config = {
+ "precision": precision,
+ }
+ config.update(config_ext)
+ handler_method = await _setup(hass, mock_client, config, get_write_api)
+
+ value = "1.9"
+ attrs = {
+ "unit_of_measurement": "foobars",
+ }
+ state = MagicMock(
+ state=value,
+ domain="fake",
+ entity_id="fake.entity-id",
+ object_id="entity",
+ attributes=attrs,
+ )
+ event = MagicMock(data={"new_state": state}, time_fired=12345)
+ body = [
+ {
+ "measurement": "foobars",
+ "tags": {"domain": "fake", "entity_id": "entity"},
+ "time": 12345,
+ "fields": {"value": float(value)},
+ }
+ ]
+ handler_method(event)
+ hass.data[influxdb.DOMAIN].block_till_done()
+
+ write_api = get_write_api(mock_client)
+ assert write_api.call_count == 1
+ assert write_api.call_args == get_mock_call(body, precision)
+ write_api.reset_mock()
diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py
index 4ffd6e4f25d480..3b966c9e8611b0 100644
--- a/tests/components/islamic_prayer_times/test_config_flow.py
+++ b/tests/components/islamic_prayer_times/test_config_flow.py
@@ -83,4 +83,4 @@ async def test_integration_already_configured(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py
index 4fd61ede8bad7c..71c2bce13074a8 100644
--- a/tests/components/kodi/test_config_flow.py
+++ b/tests/components/kodi/test_config_flow.py
@@ -165,6 +165,51 @@ async def test_form_valid_ws_port(hass, user_flow):
assert len(mock_setup_entry.mock_calls) == 1
+async def test_form_empty_ws_port(hass, user_flow):
+ """Test we handle an empty websocket port input."""
+ with patch(
+ "homeassistant.components.kodi.config_flow.Kodi.ping",
+ return_value=True,
+ ), patch.object(
+ MockWSConnection,
+ "connect",
+ AsyncMock(side_effect=CannotConnectError),
+ ), patch(
+ "homeassistant.components.kodi.config_flow.get_kodi_connection",
+ new=get_kodi_connection,
+ ):
+ result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "ws_port"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.kodi.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kodi.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"ws_port": 0}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TEST_HOST["host"]
+ assert result["data"] == {
+ **TEST_HOST,
+ "ws_port": None,
+ "password": None,
+ "username": None,
+ "name": None,
+ "timeout": DEFAULT_TIMEOUT,
+ }
+
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
async def test_form_invalid_auth(hass, user_flow):
"""Test we handle invalid auth."""
with patch(
diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py
index 5819f2128c85cd..de2d69212efa7a 100644
--- a/tests/components/kodi/test_device_trigger.py
+++ b/tests/components/kodi/test_device_trigger.py
@@ -112,6 +112,7 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
]
},
)
+ await hass.async_block_till_done()
await hass.services.async_call(
MP_DOMAIN,
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index a3f24bef3c97f0..e723ac77e0349d 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -2,15 +2,16 @@
# pylint: disable=protected-access
from io import StringIO
import os
-import unittest
import pytest
+import voluptuous as vol
from homeassistant import core
from homeassistant.components import light
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PLATFORM,
+ ENTITY_MATCH_ALL,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -18,479 +19,606 @@
STATE_ON,
)
from homeassistant.exceptions import Unauthorized
-from homeassistant.setup import async_setup_component, setup_component
+from homeassistant.setup import async_setup_component
import tests.async_mock as mock
-from tests.common import get_test_home_assistant, mock_service, mock_storage
-from tests.components.light import common
-
-
-class TestLight(unittest.TestCase):
- """Test the light module."""
-
- # pylint: disable=invalid-name
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
-
- if os.path.isfile(user_light_file):
- os.remove(user_light_file)
-
- def test_methods(self):
- """Test if methods call the services as expected."""
- # Test is_on
- self.hass.states.set("light.test", STATE_ON)
- assert light.is_on(self.hass, "light.test")
-
- self.hass.states.set("light.test", STATE_OFF)
- assert not light.is_on(self.hass, "light.test")
-
- # Test turn_on
- turn_on_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_ON)
-
- common.turn_on(
- self.hass,
- entity_id="entity_id_val",
- transition="transition_val",
- brightness="brightness_val",
- rgb_color="rgb_color_val",
- xy_color="xy_color_val",
- profile="profile_val",
- color_name="color_name_val",
- white_value="white_val",
- )
+from tests.common import async_mock_service
- self.hass.block_till_done()
- assert 1 == len(turn_on_calls)
- call = turn_on_calls[-1]
+@pytest.fixture
+def mock_storage(hass, hass_storage):
+ """Clean up user light files at the end."""
+ yield
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
- assert light.DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert "entity_id_val" == call.data.get(ATTR_ENTITY_ID)
- assert "transition_val" == call.data.get(light.ATTR_TRANSITION)
- assert "brightness_val" == call.data.get(light.ATTR_BRIGHTNESS)
- assert "rgb_color_val" == call.data.get(light.ATTR_RGB_COLOR)
- assert "xy_color_val" == call.data.get(light.ATTR_XY_COLOR)
- assert "profile_val" == call.data.get(light.ATTR_PROFILE)
- assert "color_name_val" == call.data.get(light.ATTR_COLOR_NAME)
- assert "white_val" == call.data.get(light.ATTR_WHITE_VALUE)
+ if os.path.isfile(user_light_file):
+ os.remove(user_light_file)
- # Test turn_off
- turn_off_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_OFF)
- common.turn_off(
- self.hass, entity_id="entity_id_val", transition="transition_val"
- )
+async def test_methods(hass):
+ """Test if methods call the services as expected."""
+ # Test is_on
+ hass.states.async_set("light.test", STATE_ON)
+ assert light.is_on(hass, "light.test")
- self.hass.block_till_done()
+ hass.states.async_set("light.test", STATE_OFF)
+ assert not light.is_on(hass, "light.test")
- assert 1 == len(turn_off_calls)
- call = turn_off_calls[-1]
+ # Test turn_on
+ turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- assert light.DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert "entity_id_val" == call.data[ATTR_ENTITY_ID]
- assert "transition_val" == call.data[light.ATTR_TRANSITION]
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "entity_id_val",
+ light.ATTR_TRANSITION: "transition_val",
+ light.ATTR_BRIGHTNESS: "brightness_val",
+ light.ATTR_RGB_COLOR: "rgb_color_val",
+ light.ATTR_XY_COLOR: "xy_color_val",
+ light.ATTR_PROFILE: "profile_val",
+ light.ATTR_COLOR_NAME: "color_name_val",
+ light.ATTR_WHITE_VALUE: "white_val",
+ },
+ blocking=True,
+ )
- # Test toggle
- toggle_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TOGGLE)
+ assert len(turn_on_calls) == 1
+ call = turn_on_calls[-1]
- common.toggle(self.hass, entity_id="entity_id_val", transition="transition_val")
+ assert call.domain == light.DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data.get(ATTR_ENTITY_ID) == "entity_id_val"
+ assert call.data.get(light.ATTR_TRANSITION) == "transition_val"
+ assert call.data.get(light.ATTR_BRIGHTNESS) == "brightness_val"
+ assert call.data.get(light.ATTR_RGB_COLOR) == "rgb_color_val"
+ assert call.data.get(light.ATTR_XY_COLOR) == "xy_color_val"
+ assert call.data.get(light.ATTR_PROFILE) == "profile_val"
+ assert call.data.get(light.ATTR_COLOR_NAME) == "color_name_val"
+ assert call.data.get(light.ATTR_WHITE_VALUE) == "white_val"
- self.hass.block_till_done()
+ # Test turn_off
+ turn_off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF)
- assert 1 == len(toggle_calls)
- call = toggle_calls[-1]
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {
+ ATTR_ENTITY_ID: "entity_id_val",
+ light.ATTR_TRANSITION: "transition_val",
+ },
+ blocking=True,
+ )
- assert light.DOMAIN == call.domain
- assert SERVICE_TOGGLE == call.service
- assert "entity_id_val" == call.data[ATTR_ENTITY_ID]
- assert "transition_val" == call.data[light.ATTR_TRANSITION]
+ assert len(turn_off_calls) == 1
+ call = turn_off_calls[-1]
- def test_services(self):
- """Test the provided services."""
- platform = getattr(self.hass.components, "test.light")
+ assert call.domain == light.DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data[ATTR_ENTITY_ID] == "entity_id_val"
+ assert call.data[light.ATTR_TRANSITION] == "transition_val"
- platform.init()
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
+ # Test toggle
+ toggle_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TOGGLE)
- ent1, ent2, ent3 = platform.ENTITIES
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: "entity_id_val", light.ATTR_TRANSITION: "transition_val"},
+ blocking=True,
+ )
- # Test init
- assert light.is_on(self.hass, ent1.entity_id)
- assert not light.is_on(self.hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent3.entity_id)
+ assert len(toggle_calls) == 1
+ call = toggle_calls[-1]
- # Test basic turn_on, turn_off, toggle services
- common.turn_off(self.hass, entity_id=ent1.entity_id)
- common.turn_on(self.hass, entity_id=ent2.entity_id)
+ assert call.domain == light.DOMAIN
+ assert call.service == SERVICE_TOGGLE
+ assert call.data[ATTR_ENTITY_ID] == "entity_id_val"
+ assert call.data[light.ATTR_TRANSITION] == "transition_val"
- self.hass.block_till_done()
- assert not light.is_on(self.hass, ent1.entity_id)
- assert light.is_on(self.hass, ent2.entity_id)
+async def test_services(hass):
+ """Test the provided services."""
+ platform = getattr(hass.components, "test.light")
- # turn on all lights
- common.turn_on(self.hass)
+ platform.init()
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
- self.hass.block_till_done()
+ ent1, ent2, ent3 = platform.ENTITIES
- assert light.is_on(self.hass, ent1.entity_id)
- assert light.is_on(self.hass, ent2.entity_id)
- assert light.is_on(self.hass, ent3.entity_id)
+ # Test init
+ assert light.is_on(hass, ent1.entity_id)
+ assert not light.is_on(hass, ent2.entity_id)
+ assert not light.is_on(hass, ent3.entity_id)
- # turn off all lights
- common.turn_off(self.hass)
+ # Test basic turn_on, turn_off, toggle services
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ent1.entity_id}, blocking=True
+ )
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent2.entity_id}, blocking=True
+ )
- self.hass.block_till_done()
+ assert not light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent1.entity_id)
- assert not light.is_on(self.hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent3.entity_id)
+ # turn on all lights
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
- # turn off all lights by setting brightness to 0
- common.turn_on(self.hass)
+ assert light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
+ assert light.is_on(hass, ent3.entity_id)
- self.hass.block_till_done()
+ # turn off all lights
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
- common.turn_on(self.hass, brightness=0)
+ assert not light.is_on(hass, ent1.entity_id)
+ assert not light.is_on(hass, ent2.entity_id)
+ assert not light.is_on(hass, ent3.entity_id)
- self.hass.block_till_done()
+ # turn off all lights by setting brightness to 0
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL, light.ATTR_BRIGHTNESS: 0},
+ blocking=True,
+ )
- assert not light.is_on(self.hass, ent1.entity_id)
- assert not light.is_on(self.hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent3.entity_id)
+ assert not light.is_on(hass, ent1.entity_id)
+ assert not light.is_on(hass, ent2.entity_id)
+ assert not light.is_on(hass, ent3.entity_id)
- # toggle all lights
- common.toggle(self.hass)
+ # toggle all lights
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
- self.hass.block_till_done()
+ assert light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
+ assert light.is_on(hass, ent3.entity_id)
- assert light.is_on(self.hass, ent1.entity_id)
- assert light.is_on(self.hass, ent2.entity_id)
- assert light.is_on(self.hass, ent3.entity_id)
+ # toggle all lights
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
- # toggle all lights
- common.toggle(self.hass)
+ assert not light.is_on(hass, ent1.entity_id)
+ assert not light.is_on(hass, ent2.entity_id)
+ assert not light.is_on(hass, ent3.entity_id)
- self.hass.block_till_done()
+ # Ensure all attributes process correctly
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent1.entity_id,
+ light.ATTR_TRANSITION: 10,
+ light.ATTR_BRIGHTNESS: 20,
+ light.ATTR_COLOR_NAME: "blue",
+ },
+ blocking=True,
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent2.entity_id,
+ light.ATTR_RGB_COLOR: (255, 255, 255),
+ light.ATTR_WHITE_VALUE: 255,
+ },
+ blocking=True,
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent3.entity_id,
+ light.ATTR_XY_COLOR: (0.4, 0.6),
+ },
+ blocking=True,
+ )
- assert not light.is_on(self.hass, ent1.entity_id)
- assert not light.is_on(self.hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent3.entity_id)
+ _, data = ent1.last_call("turn_on")
+ assert data == {
+ light.ATTR_TRANSITION: 10,
+ light.ATTR_BRIGHTNESS: 20,
+ light.ATTR_HS_COLOR: (240, 100),
+ }
- # Ensure all attributes process correctly
- common.turn_on(
- self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue"
- )
- common.turn_on(
- self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255
- )
- common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6))
+ _, data = ent2.last_call("turn_on")
+ assert data == {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255}
- self.hass.block_till_done()
+ _, data = ent3.last_call("turn_on")
+ assert data == {light.ATTR_HS_COLOR: (71.059, 100)}
- _, data = ent1.last_call("turn_on")
- assert {
+ # Ensure attributes are filtered when light is turned off
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent1.entity_id,
light.ATTR_TRANSITION: 10,
- light.ATTR_BRIGHTNESS: 20,
- light.ATTR_HS_COLOR: (240, 100),
- } == data
-
- _, data = ent2.last_call("turn_on")
- assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data
+ light.ATTR_BRIGHTNESS: 0,
+ light.ATTR_COLOR_NAME: "blue",
+ },
+ blocking=True,
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent2.entity_id,
+ light.ATTR_BRIGHTNESS: 0,
+ light.ATTR_RGB_COLOR: (255, 255, 255),
+ light.ATTR_WHITE_VALUE: 0,
+ },
+ blocking=True,
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent3.entity_id,
+ light.ATTR_BRIGHTNESS: 0,
+ light.ATTR_XY_COLOR: (0.4, 0.6),
+ },
+ blocking=True,
+ )
- _, data = ent3.last_call("turn_on")
- assert {light.ATTR_HS_COLOR: (71.059, 100)} == data
+ assert not light.is_on(hass, ent1.entity_id)
+ assert not light.is_on(hass, ent2.entity_id)
+ assert not light.is_on(hass, ent3.entity_id)
- # Ensure attributes are filtered when light is turned off
- common.turn_on(
- self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue"
- )
- common.turn_on(
- self.hass,
- ent2.entity_id,
- brightness=0,
- rgb_color=(255, 255, 255),
- white_value=0,
- )
- common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6))
+ _, data = ent1.last_call("turn_off")
+ assert data == {light.ATTR_TRANSITION: 10}
- self.hass.block_till_done()
+ _, data = ent2.last_call("turn_off")
+ assert data == {}
- assert not light.is_on(self.hass, ent1.entity_id)
- assert not light.is_on(self.hass, ent2.entity_id)
- assert not light.is_on(self.hass, ent3.entity_id)
+ _, data = ent3.last_call("turn_off")
+ assert data == {}
- _, data = ent1.last_call("turn_off")
- assert {light.ATTR_TRANSITION: 10} == data
+ # One of the light profiles
+ prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0
- _, data = ent2.last_call("turn_off")
- assert {} == data
+ # Test light profiles
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: prof_name},
+ blocking=True,
+ )
+ # Specify a profile and a brightness attribute to overwrite it
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent2.entity_id,
+ light.ATTR_PROFILE: prof_name,
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_TRANSITION: 1,
+ },
+ blocking=True,
+ )
- _, data = ent3.last_call("turn_off")
- assert {} == data
+ _, data = ent1.last_call("turn_on")
+ assert data == {
+ light.ATTR_BRIGHTNESS: prof_bri,
+ light.ATTR_HS_COLOR: (prof_h, prof_s),
+ light.ATTR_TRANSITION: prof_t,
+ }
+
+ _, data = ent2.last_call("turn_on")
+ assert data == {
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_HS_COLOR: (prof_h, prof_s),
+ light.ATTR_TRANSITION: 1,
+ }
+
+ # Test toggle with parameters
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TOGGLE,
+ {
+ ATTR_ENTITY_ID: ent3.entity_id,
+ light.ATTR_PROFILE: prof_name,
+ light.ATTR_BRIGHTNESS_PCT: 100,
+ },
+ blocking=True,
+ )
- # One of the light profiles
- prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0
+ _, data = ent3.last_call("turn_on")
+ assert data == {
+ light.ATTR_BRIGHTNESS: 255,
+ light.ATTR_HS_COLOR: (prof_h, prof_s),
+ light.ATTR_TRANSITION: prof_t,
+ }
- # Test light profiles
- common.turn_on(self.hass, ent1.entity_id, profile=prof_name)
- # Specify a profile and a brightness attribute to overwrite it
- common.turn_on(
- self.hass, ent2.entity_id, profile=prof_name, brightness=100, transition=1
+ # Test bad data
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: -1},
+ blocking=True,
+ )
+ with pytest.raises(vol.MultipleInvalid):
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_XY_COLOR: ["bla-di-bla", 5]},
+ blocking=True,
+ )
+ with pytest.raises(vol.MultipleInvalid):
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent3.entity_id, light.ATTR_RGB_COLOR: [255, None, 2]},
+ blocking=True,
)
- self.hass.block_till_done()
+ _, data = ent1.last_call("turn_on")
+ assert data == {}
- _, data = ent1.last_call("turn_on")
- assert {
- light.ATTR_BRIGHTNESS: prof_bri,
- light.ATTR_HS_COLOR: (prof_h, prof_s),
- light.ATTR_TRANSITION: prof_t,
- } == data
+ _, data = ent2.last_call("turn_on")
+ assert data == {}
- _, data = ent2.last_call("turn_on")
- assert {
- light.ATTR_BRIGHTNESS: 100,
- light.ATTR_HS_COLOR: (prof_h, prof_s),
- light.ATTR_TRANSITION: 1,
- } == data
-
- # Test toggle with parameters
- common.toggle(self.hass, ent3.entity_id, profile=prof_name, brightness_pct=100)
- self.hass.block_till_done()
- _, data = ent3.last_call("turn_on")
- assert {
- light.ATTR_BRIGHTNESS: 255,
- light.ATTR_HS_COLOR: (prof_h, prof_s),
- light.ATTR_TRANSITION: prof_t,
- } == data
-
- # Test bad data
- common.turn_on(self.hass)
- common.turn_on(self.hass, ent1.entity_id, profile="nonexisting")
- common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5])
- common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2])
-
- self.hass.block_till_done()
-
- _, data = ent1.last_call("turn_on")
- assert {} == data
-
- _, data = ent2.last_call("turn_on")
- assert {} == data
-
- _, data = ent3.last_call("turn_on")
- assert {} == data
-
- # faulty attributes will not trigger a service call
- common.turn_on(
- self.hass, ent1.entity_id, profile=prof_name, brightness="bright"
+ _, data = ent3.last_call("turn_on")
+ assert data == {}
+
+ # faulty attributes will not trigger a service call
+ with pytest.raises(vol.MultipleInvalid):
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent1.entity_id,
+ light.ATTR_PROFILE: prof_name,
+ light.ATTR_BRIGHTNESS: "bright",
+ },
+ blocking=True,
+ )
+ with pytest.raises(vol.MultipleInvalid):
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent1.entity_id,
+ light.ATTR_RGB_COLOR: "yellowish",
+ },
+ blocking=True,
+ )
+ with pytest.raises(vol.MultipleInvalid):
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_WHITE_VALUE: "high"},
+ blocking=True,
)
- common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish")
- common.turn_on(self.hass, ent2.entity_id, white_value="high")
- self.hass.block_till_done()
+ _, data = ent1.last_call("turn_on")
+ assert data == {}
- _, data = ent1.last_call("turn_on")
- assert {} == data
+ _, data = ent2.last_call("turn_on")
+ assert data == {}
- _, data = ent2.last_call("turn_on")
- assert {} == data
- def test_broken_light_profiles(self):
- """Test light profiles."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
+async def test_broken_light_profiles(hass, mock_storage):
+ """Test light profiles."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
- # Setup a wrong light file
- with open(user_light_file, "w") as user_file:
- user_file.write("id,x,y,brightness,transition\n")
- user_file.write("I,WILL,NOT,WORK,EVER\n")
+ # Setup a wrong light file
+ with open(user_light_file, "w") as user_file:
+ user_file.write("id,x,y,brightness,transition\n")
+ user_file.write("I,WILL,NOT,WORK,EVER\n")
- assert not setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
+ assert not await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
- def test_light_profiles(self):
- """Test light profiles."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
+async def test_light_profiles(hass, mock_storage):
+ """Test light profiles."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- with open(user_light_file, "w") as user_file:
- user_file.write("id,x,y,brightness\n")
- user_file.write("test,.4,.6,100\n")
- user_file.write("test_off,0,0,0\n")
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
+ with open(user_light_file, "w") as user_file:
+ user_file.write("id,x,y,brightness\n")
+ user_file.write("test,.4,.6,100\n")
+ user_file.write("test_off,0,0,0\n")
- ent1, _, _ = platform.ENTITIES
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
- common.turn_on(self.hass, ent1.entity_id, profile="test")
+ ent1, _, _ = platform.ENTITIES
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: ent1.entity_id,
+ light.ATTR_PROFILE: "test",
+ },
+ blocking=True,
+ )
- _, data = ent1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
+ assert light.is_on(hass, ent1.entity_id)
+ assert data == {
+ light.ATTR_HS_COLOR: (71.059, 100),
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_TRANSITION: 0,
+ }
- assert light.is_on(self.hass, ent1.entity_id)
- assert {
- light.ATTR_HS_COLOR: (71.059, 100),
- light.ATTR_BRIGHTNESS: 100,
- light.ATTR_TRANSITION: 0,
- } == data
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"},
+ blocking=True,
+ )
- common.turn_on(self.hass, ent1.entity_id, profile="test_off")
+ _, data = ent1.last_call("turn_off")
+ assert not light.is_on(hass, ent1.entity_id)
+ assert data == {light.ATTR_TRANSITION: 0}
- self.hass.block_till_done()
- _, data = ent1.last_call("turn_off")
+async def test_light_profiles_with_transition(hass, mock_storage):
+ """Test light profiles with transition."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- assert not light.is_on(self.hass, ent1.entity_id)
- assert {light.ATTR_TRANSITION: 0} == data
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
- def test_light_profiles_with_transition(self):
- """Test light profiles with transition."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
+ with open(user_light_file, "w") as user_file:
+ user_file.write("id,x,y,brightness,transition\n")
+ user_file.write("test,.4,.6,100,2\n")
+ user_file.write("test_off,0,0,0,0\n")
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
- with open(user_light_file, "w") as user_file:
- user_file.write("id,x,y,brightness,transition\n")
- user_file.write("test,.4,.6,100,2\n")
- user_file.write("test_off,0,0,0,0\n")
+ ent1, _, _ = platform.ENTITIES
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test"},
+ blocking=True,
+ )
- ent1, _, _ = platform.ENTITIES
+ _, data = ent1.last_call("turn_on")
+ assert light.is_on(hass, ent1.entity_id)
+ assert data == {
+ light.ATTR_HS_COLOR: (71.059, 100),
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_TRANSITION: 2,
+ }
- common.turn_on(self.hass, ent1.entity_id, profile="test")
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"},
+ blocking=True,
+ )
- self.hass.block_till_done()
+ _, data = ent1.last_call("turn_off")
+ assert not light.is_on(hass, ent1.entity_id)
+ assert data == {light.ATTR_TRANSITION: 0}
- _, data = ent1.last_call("turn_on")
- assert light.is_on(self.hass, ent1.entity_id)
- assert {
- light.ATTR_HS_COLOR: (71.059, 100),
- light.ATTR_BRIGHTNESS: 100,
- light.ATTR_TRANSITION: 2,
- } == data
+async def test_default_profiles_group(hass, mock_storage):
+ """Test default turn-on light profile for all lights."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- common.turn_on(self.hass, ent1.entity_id, profile="test_off")
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
+ real_isfile = os.path.isfile
+ real_open = open
+
+ def _mock_isfile(path):
+ if path == user_light_file:
+ return True
+ return real_isfile(path)
+
+ def _mock_open(path, *args, **kwargs):
+ if path == user_light_file:
+ return StringIO(profile_data)
+ return real_open(path, *args, **kwargs)
+
+ profile_data = "id,x,y,brightness,transition\ngroup.all_lights.default,.4,.6,99,2\n"
+ with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch(
+ "builtins.open", side_effect=_mock_open
+ ):
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
- self.hass.block_till_done()
+ ent, _, _ = platform.ENTITIES
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True
+ )
- _, data = ent1.last_call("turn_off")
+ _, data = ent.last_call("turn_on")
+ assert data == {
+ light.ATTR_HS_COLOR: (71.059, 100),
+ light.ATTR_BRIGHTNESS: 99,
+ light.ATTR_TRANSITION: 2,
+ }
- assert not light.is_on(self.hass, ent1.entity_id)
- assert {light.ATTR_TRANSITION: 0} == data
- def test_default_profiles_group(self):
- """Test default turn-on light profile for all lights."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
+async def test_default_profiles_light(hass, mock_storage):
+ """Test default turn-on light profile for a specific light."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
- real_isfile = os.path.isfile
- real_open = open
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
+ real_isfile = os.path.isfile
+ real_open = open
- def _mock_isfile(path):
- if path == user_light_file:
- return True
- return real_isfile(path)
+ def _mock_isfile(path):
+ if path == user_light_file:
+ return True
+ return real_isfile(path)
- def _mock_open(path, *args, **kwargs):
- if path == user_light_file:
- return StringIO(profile_data)
- return real_open(path, *args, **kwargs)
+ def _mock_open(path, *args, **kwargs):
+ if path == user_light_file:
+ return StringIO(profile_data)
+ return real_open(path, *args, **kwargs)
- profile_data = (
- "id,x,y,brightness,transition\ngroup.all_lights.default,.4,.6,99,2\n"
- )
- with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch(
- "builtins.open", side_effect=_mock_open
- ), mock_storage():
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
-
- ent, _, _ = platform.ENTITIES
- common.turn_on(self.hass, ent.entity_id)
- self.hass.block_till_done()
- _, data = ent.last_call("turn_on")
- assert {
- light.ATTR_HS_COLOR: (71.059, 100),
- light.ATTR_BRIGHTNESS: 99,
- light.ATTR_TRANSITION: 2,
- } == data
-
- def test_default_profiles_light(self):
- """Test default turn-on light profile for a specific light."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
-
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
- real_isfile = os.path.isfile
- real_open = open
-
- def _mock_isfile(path):
- if path == user_light_file:
- return True
- return real_isfile(path)
-
- def _mock_open(path, *args, **kwargs):
- if path == user_light_file:
- return StringIO(profile_data)
- return real_open(path, *args, **kwargs)
-
- profile_data = (
- "id,x,y,brightness,transition\n"
- + "group.all_lights.default,.3,.5,200,0\n"
- + "light.ceiling_2.default,.6,.6,100,3\n"
- )
- with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch(
- "builtins.open", side_effect=_mock_open
- ), mock_storage():
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
-
- dev = next(
- filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)
+ profile_data = (
+ "id,x,y,brightness,transition\n"
+ + "group.all_lights.default,.3,.5,200,0\n"
+ + "light.ceiling_2.default,.6,.6,100,3\n"
+ )
+ with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch(
+ "builtins.open", side_effect=_mock_open
+ ):
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- common.turn_on(self.hass, dev.entity_id)
- self.hass.block_till_done()
- _, data = dev.last_call("turn_on")
- assert {
- light.ATTR_HS_COLOR: (50.353, 100),
- light.ATTR_BRIGHTNESS: 100,
- light.ATTR_TRANSITION: 3,
- } == data
+ await hass.async_block_till_done()
+
+ dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES))
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: dev.entity_id,
+ },
+ blocking=True,
+ )
+
+ _, data = dev.last_call("turn_on")
+ assert data == {
+ light.ATTR_HS_COLOR: (50.353, 100),
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_TRANSITION: 3,
+ }
async def test_light_context(hass, hass_admin_user):
@@ -507,8 +635,8 @@ async def test_light_context(hass, hass_admin_user):
"light",
"toggle",
{"entity_id": state.entity_id},
- True,
- core.Context(user_id=hass_admin_user.id),
+ blocking=True,
+ context=core.Context(user_id=hass_admin_user.id),
)
state2 = hass.states.get("light.ceiling")
@@ -534,8 +662,8 @@ async def test_light_turn_on_auth(hass, hass_admin_user):
"light",
"turn_on",
{"entity_id": state.entity_id},
- True,
- core.Context(user_id=hass_admin_user.id),
+ blocking=True,
+ context=core.Context(user_id=hass_admin_user.id),
)
@@ -557,7 +685,7 @@ async def test_light_brightness_step(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_step": -10},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -567,7 +695,7 @@ async def test_light_brightness_step(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_step_pct": 10},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -592,7 +720,7 @@ async def test_light_brightness_pct_conversion(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_pct": 1},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -602,7 +730,7 @@ async def test_light_brightness_pct_conversion(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_pct": 2},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -612,7 +740,7 @@ async def test_light_brightness_pct_conversion(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_pct": 50},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -622,7 +750,7 @@ async def test_light_brightness_pct_conversion(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_pct": 99},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
@@ -632,7 +760,7 @@ async def test_light_brightness_pct_conversion(hass):
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_pct": 100},
- True,
+ blocking=True,
)
_, data = entity.last_call("turn_on")
diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py
index 5e41f0bce8957a..96a5634d350e35 100644
--- a/tests/components/logbook/test_init.py
+++ b/tests/components/logbook/test_init.py
@@ -9,7 +9,7 @@
import pytest
import voluptuous as vol
-from homeassistant.components import logbook, recorder, sun
+from homeassistant.components import logbook, recorder
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
@@ -26,17 +26,14 @@
CONF_INCLUDE,
EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_START,
+ EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
- STATE_NOT_HOME,
STATE_OFF,
STATE_ON,
)
import homeassistant.core as ha
-from homeassistant.helpers.entityfilter import (
- CONF_ENTITY_GLOBS,
- convert_include_exclude_filter,
-)
+from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS
from homeassistant.helpers.json import JSONEncoder
from homeassistant.setup import async_setup_component, setup_component
import homeassistant.util.dt as dt_util
@@ -94,6 +91,7 @@ def event_listener(event):
# Logbook entry service call results in firing an event.
# Our service call will unblock when the event listeners have been
# scheduled. This means that they may not have been processed yet.
+ trigger_db_commit(self.hass)
self.hass.block_till_done()
self.hass.data[recorder.DATA_INSTANCE].block_till_done()
@@ -159,420 +157,9 @@ def test_humanify_filter_sensor(self):
)
assert len(entries) == 2
- self.assert_entry(
- entries[0], pointB, "bla", domain="sensor", entity_id=entity_id
- )
-
- self.assert_entry(
- entries[1], pointC, "bla", domain="sensor", entity_id=entity_id
- )
-
- def test_exclude_events_entity(self):
- """Test if events are filtered if entity is excluded in config."""
- entity_id = "sensor.bla"
- entity_id2 = "sensor.blu"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}},
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
- eventA,
- eventB,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 2
- self.assert_entry(
- entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_exclude_events_domain(self):
- """Test if events are filtered if domain is excluded in config."""
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}},
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME),
- eventA,
- eventB,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 2
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_exclude_events_domain_glob(self):
- """Test if events are filtered if domain or glob is excluded in config."""
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- entity_id3 = "sensor.excluded"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
- eventC = self.create_state_changed_event(pointC, entity_id3, 30)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_EXCLUDE: {
- CONF_DOMAINS: ["switch", "alexa"],
- CONF_ENTITY_GLOBS: "*.excluded",
- }
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME),
- eventA,
- eventB,
- eventC,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 2
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_include_events_entity(self):
- """Test if events are filtered if entity is included in config."""
- entity_id = "sensor.bla"
- entity_id2 = "sensor.blu"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_INCLUDE: {
- CONF_DOMAINS: ["homeassistant"],
- CONF_ENTITIES: [entity_id2],
- }
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
- eventA,
- eventB,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 2
- self.assert_entry(
- entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_include_events_domain(self):
- """Test if events are filtered if domain is included in config."""
- assert setup_component(self.hass, "alexa", {})
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- event_alexa = MockLazyEventPartialState(
- EVENT_ALEXA_SMART_HOME,
- {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
- )
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]}
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- event_alexa,
- eventA,
- eventB,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 3
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
- self.assert_entry(
- entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_include_events_domain_glob(self):
- """Test if events are filtered if domain or glob is included in config."""
- assert setup_component(self.hass, "alexa", {})
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- entity_id3 = "switch.included"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- event_alexa = MockLazyEventPartialState(
- EVENT_ALEXA_SMART_HOME,
- {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
- )
-
- eventA = self.create_state_changed_event(pointA, entity_id, 10)
- eventB = self.create_state_changed_event(pointB, entity_id2, 20)
- eventC = self.create_state_changed_event(pointC, entity_id3, 30)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_INCLUDE: {
- CONF_DOMAINS: ["homeassistant", "sensor", "alexa"],
- CONF_ENTITY_GLOBS: ["*.included"],
- }
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- event_alexa,
- eventA,
- eventB,
- eventC,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 4
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
- self.assert_entry(
- entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
- self.assert_entry(
- entries[3], pointC, "included", domain="switch", entity_id=entity_id3
- )
-
- def test_include_exclude_events(self):
- """Test if events are filtered if include and exclude is configured."""
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- entity_id3 = "sensor.bli"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA1 = self.create_state_changed_event(pointA, entity_id, 10)
- eventA2 = self.create_state_changed_event(pointA, entity_id2, 10)
- eventA3 = self.create_state_changed_event(pointA, entity_id3, 10)
- eventB1 = self.create_state_changed_event(pointB, entity_id, 20)
- eventB2 = self.create_state_changed_event(pointB, entity_id2, 20)
+ self.assert_entry(entries[0], pointB, "bla", entity_id=entity_id)
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_INCLUDE: {
- CONF_DOMAINS: ["sensor", "homeassistant"],
- CONF_ENTITIES: ["switch.bla"],
- },
- CONF_EXCLUDE: {
- CONF_DOMAINS: ["switch"],
- CONF_ENTITIES: ["sensor.bli"],
- },
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- eventA1,
- eventA2,
- eventA3,
- eventB1,
- eventB2,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 5
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointA, "bla", domain="switch", entity_id=entity_id
- )
- self.assert_entry(
- entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2
- )
- self.assert_entry(
- entries[3], pointB, "bla", domain="switch", entity_id=entity_id
- )
- self.assert_entry(
- entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
-
- def test_include_exclude_events_with_glob_filters(self):
- """Test if events are filtered if include and exclude is configured."""
- entity_id = "switch.bla"
- entity_id2 = "sensor.blu"
- entity_id3 = "sensor.bli"
- entity_id4 = "light.included"
- entity_id5 = "switch.included"
- entity_id6 = "sensor.excluded"
- pointA = dt_util.utcnow()
- pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- eventA1 = self.create_state_changed_event(pointA, entity_id, 10)
- eventA2 = self.create_state_changed_event(pointA, entity_id2, 10)
- eventA3 = self.create_state_changed_event(pointA, entity_id3, 10)
- eventB1 = self.create_state_changed_event(pointB, entity_id, 20)
- eventB2 = self.create_state_changed_event(pointB, entity_id2, 20)
- eventC1 = self.create_state_changed_event(pointC, entity_id4, 30)
- eventC2 = self.create_state_changed_event(pointC, entity_id5, 30)
- eventC3 = self.create_state_changed_event(pointC, entity_id6, 30)
-
- config = logbook.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- logbook.DOMAIN: {
- CONF_INCLUDE: {
- CONF_DOMAINS: ["sensor", "homeassistant"],
- CONF_ENTITIES: ["switch.bla"],
- CONF_ENTITY_GLOBS: ["*.included"],
- },
- CONF_EXCLUDE: {
- CONF_DOMAINS: ["switch"],
- CONF_ENTITY_GLOBS: ["*.excluded"],
- CONF_ENTITIES: ["sensor.bli"],
- },
- },
- }
- )
- entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
- events = [
- e
- for e in (
- MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
- eventA1,
- eventA2,
- eventA3,
- eventB1,
- eventB2,
- eventC1,
- eventC2,
- eventC3,
- )
- if logbook._keep_event(self.hass, e, entities_filter)
- ]
- entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
-
- assert len(entries) == 6
- self.assert_entry(
- entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
- )
- self.assert_entry(
- entries[1], pointA, "bla", domain="switch", entity_id=entity_id
- )
- self.assert_entry(
- entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2
- )
- self.assert_entry(
- entries[3], pointB, "bla", domain="switch", entity_id=entity_id
- )
- self.assert_entry(
- entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2
- )
- self.assert_entry(
- entries[5], pointC, "included", domain="light", entity_id=entity_id4
- )
+ self.assert_entry(entries[1], pointC, "bla", entity_id=entity_id)
def test_home_assistant_start_stop_grouped(self):
"""Test if HA start and stop events are grouped.
@@ -619,724 +206,88 @@ def test_home_assistant_start(self):
self.assert_entry(
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
)
- self.assert_entry(
- entries[1], pointA, "bla", domain="switch", entity_id=entity_id
- )
+ self.assert_entry(entries[1], pointA, "bla", entity_id=entity_id)
- def test_entry_message_from_event_device(self):
- """Test if logbook message is correctly created for switches.
-
- Especially test if the special handling for turn on/off events is done.
- """
- pointA = dt_util.utcnow()
+ def test_process_custom_logbook_entries(self):
+ """Test if custom log book entries get added as an entry."""
+ name = "Nice name"
+ message = "has a custom entry"
+ entity_id = "sun.sun"
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
- # message for a device state change
- eventA = self.create_state_changed_event(pointA, "switch.bla", 10)
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "changed to 10"
-
- # message for a switch turned on
- eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_ON)
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "turned on"
- # message for a switch turned off
- eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_OFF)
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
+ entries = list(
+ logbook.humanify(
+ self.hass,
+ (
+ MockLazyEventPartialState(
+ logbook.EVENT_LOGBOOK_ENTRY,
+ {
+ logbook.ATTR_NAME: name,
+ logbook.ATTR_MESSAGE: message,
+ logbook.ATTR_ENTITY_ID: entity_id,
+ },
+ ),
+ ),
+ entity_attr_cache,
+ {},
+ )
)
- assert message == "turned off"
-
- def test_entry_message_from_event_device_tracker(self):
- """Test if logbook message is correctly created for device tracker."""
- pointA = dt_util.utcnow()
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
- # message for a device tracker "not home" state
- eventA = self.create_state_changed_event(
- pointA, "device_tracker.john", STATE_NOT_HOME
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is away"
+ assert len(entries) == 1
+ self.assert_entry(entries[0], name=name, message=message, entity_id=entity_id)
- # message for a device tracker "home" state
- eventA = self.create_state_changed_event(pointA, "device_tracker.john", "work")
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is at work"
+ # pylint: disable=no-self-use
+ def assert_entry(
+ self, entry, when=None, name=None, message=None, domain=None, entity_id=None
+ ):
+ """Assert an entry is what is expected."""
+ return _assert_entry(entry, when, name, message, domain, entity_id)
- def test_entry_message_from_event_person(self):
- """Test if logbook message is correctly created for a person."""
- pointA = dt_util.utcnow()
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
+ def create_state_changed_event(
+ self,
+ event_time_fired,
+ entity_id,
+ state,
+ attributes=None,
+ last_changed=None,
+ last_updated=None,
+ ):
+ """Create state changed event."""
+ old_state = ha.State(
+ entity_id, "old", attributes, last_changed, last_updated
+ ).as_dict()
+ new_state = ha.State(
+ entity_id, state, attributes, last_changed, last_updated
+ ).as_dict()
- # message for a device tracker "not home" state
- eventA = self.create_state_changed_event(pointA, "person.john", STATE_NOT_HOME)
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
+ return self.create_state_changed_event_from_old_new(
+ entity_id, event_time_fired, old_state, new_state
)
- assert message == "is away"
- # message for a device tracker "home" state
- eventA = self.create_state_changed_event(pointA, "person.john", "work")
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is at work"
-
- def test_entry_message_from_event_sun(self):
- """Test if logbook message is correctly created for sun."""
- pointA = dt_util.utcnow()
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a sun rise
- eventA = self.create_state_changed_event(
- pointA, "sun.sun", sun.STATE_ABOVE_HORIZON
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "has risen"
-
- # message for a sun set
- eventA = self.create_state_changed_event(
- pointA, "sun.sun", sun.STATE_BELOW_HORIZON
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "has set"
-
- def test_entry_message_from_event_binary_sensor_battery(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "battery"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor battery "low" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.battery", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is low"
-
- # message for a binary_sensor battery "normal" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.battery", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is normal"
-
- def test_entry_message_from_event_binary_sensor_connectivity(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "connectivity"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor connectivity "connected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.connectivity", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is connected"
-
- # message for a binary_sensor connectivity "disconnected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.connectivity", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is disconnected"
-
- def test_entry_message_from_event_binary_sensor_door(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "door"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor door "open" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.door", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is opened"
-
- # message for a binary_sensor door "closed" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.door", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is closed"
-
- def test_entry_message_from_event_binary_sensor_garage_door(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "garage_door"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor garage_door "open" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.garage_door", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is opened"
-
- # message for a binary_sensor garage_door "closed" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.garage_door", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is closed"
-
- def test_entry_message_from_event_binary_sensor_opening(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "opening"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor opening "open" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.opening", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is opened"
-
- # message for a binary_sensor opening "closed" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.opening", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is closed"
-
- def test_entry_message_from_event_binary_sensor_window(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "window"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor window "open" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.window", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is opened"
-
- # message for a binary_sensor window "closed" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.window", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is closed"
-
- def test_entry_message_from_event_binary_sensor_lock(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "lock"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor lock "unlocked" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.lock", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is unlocked"
-
- # message for a binary_sensor lock "locked" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.lock", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is locked"
-
- def test_entry_message_from_event_binary_sensor_plug(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "plug"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor plug "unpluged" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.plug", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is plugged in"
-
- # message for a binary_sensor plug "pluged" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.plug", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is unplugged"
-
- def test_entry_message_from_event_binary_sensor_presence(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "presence"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor presence "home" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.presence", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is at home"
-
- # message for a binary_sensor presence "away" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.presence", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is away"
-
- def test_entry_message_from_event_binary_sensor_safety(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "safety"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor safety "unsafe" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.safety", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is unsafe"
-
- # message for a binary_sensor safety "safe" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.safety", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "is safe"
-
- def test_entry_message_from_event_binary_sensor_cold(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "cold"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor cold "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.cold", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected cold"
-
- # message for a binary_sensori cold "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.cold", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no cold detected)"
-
- def test_entry_message_from_event_binary_sensor_gas(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "gas"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor gas "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.gas", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected gas"
-
- # message for a binary_sensori gas "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.gas", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no gas detected)"
-
- def test_entry_message_from_event_binary_sensor_heat(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "heat"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor heat "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.heat", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected heat"
-
- # message for a binary_sensori heat "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.heat", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no heat detected)"
-
- def test_entry_message_from_event_binary_sensor_light(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "light"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor light "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.light", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected light"
-
- # message for a binary_sensori light "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.light", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no light detected)"
-
- def test_entry_message_from_event_binary_sensor_moisture(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "moisture"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor moisture "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.moisture", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected moisture"
-
- # message for a binary_sensori moisture "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.moisture", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no moisture detected)"
-
- def test_entry_message_from_event_binary_sensor_motion(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "motion"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor motion "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.motion", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected motion"
-
- # message for a binary_sensori motion "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.motion", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no motion detected)"
-
- def test_entry_message_from_event_binary_sensor_occupancy(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "occupancy"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor occupancy "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.occupancy", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected occupancy"
-
- # message for a binary_sensori occupancy "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.occupancy", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no occupancy detected)"
-
- def test_entry_message_from_event_binary_sensor_power(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "power"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor power "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.power", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected power"
-
- # message for a binary_sensori power "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.power", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no power detected)"
-
- def test_entry_message_from_event_binary_sensor_problem(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "problem"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor problem "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.problem", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected problem"
-
- # message for a binary_sensori problem "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.problem", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no problem detected)"
-
- def test_entry_message_from_event_binary_sensor_smoke(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "smoke"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor smoke "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.smoke", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected smoke"
-
- # message for a binary_sensori smoke "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.smoke", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no smoke detected)"
-
- def test_entry_message_from_event_binary_sensor_sound(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "sound"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor sound "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.sound", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected sound"
-
- # message for a binary_sensori sound "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.sound", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no sound detected)"
-
- def test_entry_message_from_event_binary_sensor_vibration(self):
- """Test if logbook message is correctly created for a binary_sensor."""
- pointA = dt_util.utcnow()
- attributes = {"device_class": "vibration"}
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- # message for a binary_sensor vibration "detected" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.vibration", STATE_ON, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "detected vibration"
-
- # message for a binary_sensori vibration "cleared" state
- eventA = self.create_state_changed_event(
- pointA, "binary_sensor.vibration", STATE_OFF, attributes
- )
- message = logbook._entry_message_from_event(
- eventA.entity_id, eventA.domain, eventA, entity_attr_cache
- )
- assert message == "cleared (no vibration detected)"
-
- def test_process_custom_logbook_entries(self):
- """Test if custom log book entries get added as an entry."""
- name = "Nice name"
- message = "has a custom entry"
- entity_id = "sun.sun"
- entity_attr_cache = logbook.EntityAttributeCache(self.hass)
-
- entries = list(
- logbook.humanify(
- self.hass,
- (
- MockLazyEventPartialState(
- logbook.EVENT_LOGBOOK_ENTRY,
- {
- logbook.ATTR_NAME: name,
- logbook.ATTR_MESSAGE: message,
- logbook.ATTR_ENTITY_ID: entity_id,
- },
- ),
- ),
- entity_attr_cache,
- {},
- )
- )
-
- assert len(entries) == 1
- self.assert_entry(
- entries[0], name=name, message=message, domain="sun", entity_id=entity_id
- )
-
- # pylint: disable=no-self-use
- def assert_entry(
- self, entry, when=None, name=None, message=None, domain=None, entity_id=None
- ):
- """Assert an entry is what is expected."""
- if when:
- assert when.isoformat() == entry["when"]
-
- if name:
- assert name == entry["name"]
-
- if message:
- assert message == entry["message"]
-
- if domain:
- assert domain == entry["domain"]
-
- if entity_id:
- assert entity_id == entry["entity_id"]
-
- def create_state_changed_event(
- self,
- event_time_fired,
- entity_id,
- state,
- attributes=None,
- last_changed=None,
- last_updated=None,
- ):
- """Create state changed event."""
- old_state = ha.State(
- entity_id, "old", attributes, last_changed, last_updated
- ).as_dict()
- new_state = ha.State(
- entity_id, state, attributes, last_changed, last_updated
- ).as_dict()
-
- return self.create_state_changed_event_from_old_new(
- entity_id, event_time_fired, old_state, new_state
- )
-
- # pylint: disable=no-self-use
- def create_state_changed_event_from_old_new(
- self, entity_id, event_time_fired, old_state, new_state
- ):
- """Create a state changed event from a old and new state."""
- attributes = {}
- if new_state is not None:
- attributes = new_state.get("attributes")
- attributes_json = json.dumps(attributes, cls=JSONEncoder)
- row = collections.namedtuple(
- "Row",
- [
- "event_type"
- "event_data"
- "time_fired"
- "context_id"
- "context_user_id"
- "state"
- "entity_id"
- "domain"
- "attributes"
- "state_id",
- "old_state_id",
- ],
+ # pylint: disable=no-self-use
+ def create_state_changed_event_from_old_new(
+ self, entity_id, event_time_fired, old_state, new_state
+ ):
+ """Create a state changed event from a old and new state."""
+ attributes = {}
+ if new_state is not None:
+ attributes = new_state.get("attributes")
+ attributes_json = json.dumps(attributes, cls=JSONEncoder)
+ row = collections.namedtuple(
+ "Row",
+ [
+ "event_type"
+ "event_data"
+ "time_fired"
+ "context_id"
+ "context_user_id"
+ "state"
+ "entity_id"
+ "domain"
+ "attributes"
+ "state_id",
+ "old_state_id",
+ ],
)
row.event_type = EVENT_STATE_CHANGED
@@ -1357,7 +308,7 @@ async def test_logbook_view(hass, hass_client):
"""Test the logbook view."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
response = await client.get(f"/api/logbook/{dt_util.utcnow().isoformat()}")
assert response.status == 200
@@ -1367,7 +318,7 @@ async def test_logbook_view_period_entity(hass, hass_client):
"""Test the logbook view with period and entity."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id_test = "switch.test"
hass.states.async_set(entity_id_test, STATE_OFF)
@@ -1375,9 +326,9 @@ async def test_logbook_view_period_entity(hass, hass_client):
entity_id_second = "switch.second"
hass.states.async_set(entity_id_second, STATE_OFF)
hass.states.async_set(entity_id_second, STATE_ON)
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1473,6 +424,8 @@ def _describe(event):
):
hass.bus.async_fire("some_event")
await hass.async_block_till_done()
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
await hass.async_add_executor_job(
hass.data[recorder.DATA_INSTANCE].block_till_done
)
@@ -1541,6 +494,8 @@ def async_describe_events(hass, async_describe_event):
"some_event", {logbook.ATTR_NAME: name, logbook.ATTR_ENTITY_ID: entity_id3}
)
await hass.async_block_till_done()
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
await hass.async_add_executor_job(
hass.data[recorder.DATA_INSTANCE].block_till_done
)
@@ -1551,8 +506,6 @@ def async_describe_events(hass, async_describe_event):
assert len(results) == 1
event = results[0]
assert event["name"] == "Test Name"
- assert event["message"] == "tested a message"
- assert event["domain"] == "automation"
assert event["entity_id"] == "automation.included_rule"
@@ -1560,7 +513,7 @@ async def test_logbook_view_end_time_entity(hass, hass_client):
"""Test the logbook view with end_time and entity."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id_test = "switch.test"
hass.states.async_set(entity_id_test, STATE_OFF)
@@ -1568,9 +521,9 @@ async def test_logbook_view_end_time_entity(hass, hass_client):
entity_id_second = "switch.second"
hass.states.async_set(entity_id_second, STATE_OFF)
hass.states.async_set(entity_id_second, STATE_ON)
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1621,7 +574,7 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client):
await async_setup_component(hass, "automation", {})
await async_setup_component(hass, "script", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id_test = "alarm_control_panel.area_001"
hass.states.async_set(entity_id_test, STATE_OFF)
@@ -1640,9 +593,9 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client):
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1693,7 +646,7 @@ async def test_filter_continuous_sensor_values(hass, hass_client):
"""Test remove continuous sensor events from logbook."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id_test = "switch.test"
hass.states.async_set(entity_id_test, STATE_OFF)
@@ -1705,9 +658,9 @@ async def test_filter_continuous_sensor_values(hass, hass_client):
hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"})
hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"})
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1729,7 +682,7 @@ async def test_exclude_new_entities(hass, hass_client):
"""Test if events are excluded on first update."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id = "climate.bla"
entity_id2 = "climate.blu"
@@ -1739,9 +692,9 @@ async def test_exclude_new_entities(hass, hass_client):
hass.states.async_set(entity_id2, STATE_OFF)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1764,7 +717,7 @@ async def test_exclude_removed_entities(hass, hass_client):
"""Test if events are excluded on last update."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
entity_id = "climate.bla"
entity_id2 = "climate.blu"
@@ -1780,9 +733,9 @@ async def test_exclude_removed_entities(hass, hass_client):
hass.states.async_remove(entity_id)
hass.states.async_remove(entity_id2)
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1806,7 +759,7 @@ async def test_exclude_attribute_changes(hass, hass_client):
"""Test if events of attribute changes are filtered."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -1819,9 +772,9 @@ async def test_exclude_attribute_changes(hass, hass_client):
await hass.async_block_till_done()
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1836,9 +789,7 @@ async def test_exclude_attribute_changes(hass, hass_client):
assert len(response_json) == 3
assert response_json[0]["domain"] == "homeassistant"
- assert response_json[1]["message"] == "turned on"
assert response_json[1]["entity_id"] == "light.kitchen"
- assert response_json[2]["message"] == "turned off"
assert response_json[2]["entity_id"] == "light.kitchen"
@@ -1849,7 +800,7 @@ async def test_logbook_entity_context_id(hass, hass_client):
await async_setup_component(hass, "automation", {})
await async_setup_component(hass, "script", {})
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
context = ha.Context(
id="ac5bd62de45711eaaeb351041eec8dd9",
@@ -1889,7 +840,7 @@ async def test_logbook_entity_context_id(hass, hass_client):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
- await hass.async_add_job(
+ await hass.async_add_executor_job(
logbook.log_entry,
hass,
"mock_name",
@@ -1900,7 +851,7 @@ async def test_logbook_entity_context_id(hass, hass_client):
)
await hass.async_block_till_done()
- await hass.async_add_job(
+ await hass.async_add_executor_job(
logbook.log_entry,
hass,
"mock_name",
@@ -1935,9 +886,9 @@ async def test_logbook_entity_context_id(hass, hass_client):
)
await hass.async_block_till_done()
- await hass.async_add_job(trigger_db_commit, hass)
+ await hass.async_add_executor_job(trigger_db_commit, hass)
await hass.async_block_till_done()
- await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await hass_client()
@@ -1969,34 +920,119 @@ async def test_logbook_entity_context_id(hass, hass_client):
assert json_dict[2]["context_entity_id_name"] == "Alarm Automation"
assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
- assert json_dict[3]["entity_id"] == entity_id_second
- assert json_dict[3]["context_event_type"] == "automation_triggered"
- assert json_dict[3]["context_entity_id"] == "automation.alarm"
- assert json_dict[3]["context_entity_id_name"] == "Alarm Automation"
- assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+ assert json_dict[3]["entity_id"] == entity_id_second
+ assert json_dict[3]["context_event_type"] == "automation_triggered"
+ assert json_dict[3]["context_entity_id"] == "automation.alarm"
+ assert json_dict[3]["context_entity_id_name"] == "Alarm Automation"
+ assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+
+ assert json_dict[4]["domain"] == "homeassistant"
+
+ assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003"
+ assert json_dict[5]["context_event_type"] == "automation_triggered"
+ assert json_dict[5]["context_entity_id"] == "automation.alarm"
+ assert json_dict[5]["domain"] == "alarm_control_panel"
+ assert json_dict[5]["context_entity_id_name"] == "Alarm Automation"
+ assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+
+ assert json_dict[6]["domain"] == "homeassistant"
+ assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+
+ assert json_dict[7]["entity_id"] == "light.switch"
+ assert json_dict[7]["context_event_type"] == "call_service"
+ assert json_dict[7]["context_domain"] == "light"
+ assert json_dict[7]["context_service"] == "turn_off"
+ assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+
+
+async def test_logbook_context_from_template(hass, hass_client):
+ """Test the logbook view with end_time and entity with automations and scripts."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", {})
+ assert await async_setup_component(
+ hass,
+ "switch",
+ {
+ "switch": {
+ "platform": "template",
+ "switches": {
+ "test_template_switch": {
+ "value_template": "{{ states.switch.test_state.state }}",
+ "turn_on": {
+ "service": "switch.turn_on",
+ "entity_id": "switch.test_state",
+ },
+ "turn_off": {
+ "service": "switch.turn_off",
+ "entity_id": "switch.test_state",
+ },
+ }
+ },
+ }
+ },
+ )
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ # Entity added (should not be logged)
+ hass.states.async_set("switch.test_state", STATE_ON)
+ await hass.async_block_till_done()
+
+ # First state change (should be logged)
+ hass.states.async_set("switch.test_state", STATE_OFF)
+ await hass.async_block_till_done()
+
+ switch_turn_off_context = ha.Context(
+ id="9c5bd62de45711eaaeb351041eec8dd9",
+ user_id="9400facee45711eaa9308bfd3d19e474",
+ )
+ hass.states.async_set(
+ "switch.test_state", STATE_ON, context=switch_turn_off_context
+ )
+ await hass.async_block_till_done()
+
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ 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 == 200
+ json_dict = await response.json()
+
+ assert json_dict[0]["domain"] == "homeassistant"
+ assert "context_entity_id" not in json_dict[0]
- assert json_dict[4]["domain"] == "homeassistant"
+ assert json_dict[1]["entity_id"] == "switch.test_template_switch"
- assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003"
- assert json_dict[5]["context_event_type"] == "automation_triggered"
- assert json_dict[5]["context_entity_id"] == "automation.alarm"
- assert json_dict[5]["domain"] == "alarm_control_panel"
- assert json_dict[5]["context_entity_id_name"] == "Alarm Automation"
- assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+ assert json_dict[2]["entity_id"] == "switch.test_state"
- assert json_dict[6]["domain"] == "homeassistant"
- assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474"
+ assert json_dict[3]["entity_id"] == "switch.test_template_switch"
+ assert json_dict[3]["context_entity_id"] == "switch.test_state"
+ assert json_dict[3]["context_entity_id_name"] == "test state"
- assert json_dict[7]["entity_id"] == "light.switch"
- assert json_dict[7]["context_event_type"] == "call_service"
- assert json_dict[7]["context_domain"] == "light"
- assert json_dict[7]["context_service"] == "turn_off"
- assert json_dict[7]["domain"] == "light"
- assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+ assert json_dict[4]["entity_id"] == "switch.test_state"
+ assert json_dict[4]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+
+ assert json_dict[5]["entity_id"] == "switch.test_template_switch"
+ assert json_dict[5]["context_entity_id"] == "switch.test_state"
+ assert json_dict[5]["context_entity_id_name"] == "test state"
+ assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
-async def test_logbook_context_from_template(hass, hass_client):
- """Test the logbook view with end_time and entity with automations and scripts."""
+async def test_logbook_entity_matches_only(hass, hass_client):
+ """Test the logbook view with a single entity and entity_matches_only."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
assert await async_setup_component(
@@ -2056,33 +1092,21 @@ async def test_logbook_context_from_template(hass, hass_client):
# 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}"
+ f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state&entity_matches_only"
)
assert response.status == 200
json_dict = await response.json()
- assert json_dict[0]["domain"] == "homeassistant"
- assert "context_entity_id" not in json_dict[0]
-
- assert json_dict[1]["entity_id"] == "switch.test_template_switch"
-
- assert json_dict[2]["entity_id"] == "switch.test_state"
-
- assert json_dict[3]["entity_id"] == "switch.test_template_switch"
- assert json_dict[3]["context_entity_id"] == "switch.test_state"
- assert json_dict[3]["context_entity_id_name"] == "test state"
+ assert len(json_dict) == 2
- assert json_dict[4]["entity_id"] == "switch.test_state"
- assert json_dict[4]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+ assert json_dict[0]["entity_id"] == "switch.test_state"
- assert json_dict[5]["entity_id"] == "switch.test_template_switch"
- assert json_dict[5]["context_entity_id"] == "switch.test_state"
- assert json_dict[5]["context_entity_id_name"] == "test state"
- assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+ assert json_dict[1]["entity_id"] == "switch.test_state"
+ assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
-async def test_logbook_entity_matches_only(hass, hass_client):
- """Test the logbook view with a single entity and entity_matches_only."""
+async def test_logbook_entity_matches_only_multiple(hass, hass_client):
+ """Test the logbook view with a multiple entities and entity_matches_only."""
await hass.async_add_executor_job(init_recorder_component, hass)
await async_setup_component(hass, "logbook", {})
assert await async_setup_component(
@@ -2114,10 +1138,14 @@ async def test_logbook_entity_matches_only(hass, hass_client):
# Entity added (should not be logged)
hass.states.async_set("switch.test_state", STATE_ON)
+ hass.states.async_set("light.test_state", STATE_ON)
+
await hass.async_block_till_done()
# First state change (should be logged)
hass.states.async_set("switch.test_state", STATE_OFF)
+ hass.states.async_set("light.test_state", STATE_OFF)
+
await hass.async_block_till_done()
switch_turn_off_context = ha.Context(
@@ -2127,6 +1155,7 @@ async def test_logbook_entity_matches_only(hass, hass_client):
hass.states.async_set(
"switch.test_state", STATE_ON, context=switch_turn_off_context
)
+ hass.states.async_set("light.test_state", STATE_ON, context=switch_turn_off_context)
await hass.async_block_till_done()
await hass.async_add_executor_job(trigger_db_commit, hass)
@@ -2142,19 +1171,22 @@ async def test_logbook_entity_matches_only(hass, hass_client):
# 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}&entity=switch.test_state&entity_matches_only"
+ f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state,light.test_state&entity_matches_only"
)
assert response.status == 200
json_dict = await response.json()
- assert len(json_dict) == 2
+ assert len(json_dict) == 4
assert json_dict[0]["entity_id"] == "switch.test_state"
- assert json_dict[0]["message"] == "turned off"
- assert json_dict[1]["entity_id"] == "switch.test_state"
- assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
- assert json_dict[1]["message"] == "turned on"
+ assert json_dict[1]["entity_id"] == "light.test_state"
+
+ assert json_dict[2]["entity_id"] == "switch.test_state"
+ assert json_dict[2]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
+
+ assert json_dict[3]["entity_id"] == "light.test_state"
+ assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
async def test_logbook_invalid_entity(hass, hass_client):
@@ -2176,6 +1208,458 @@ async def test_logbook_invalid_entity(hass, hass_client):
assert response.status == 500
+async def test_icon_and_state(hass, hass_client):
+ """Test to ensure state and custom icons are returned."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", {})
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+
+ hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"})
+ hass.states.async_set(
+ "light.kitchen", STATE_ON, {"brightness": 100, "icon": "mdi:security"}
+ )
+ hass.states.async_set(
+ "light.kitchen", STATE_ON, {"brightness": 200, "icon": "mdi:security"}
+ )
+ hass.states.async_set(
+ "light.kitchen", STATE_ON, {"brightness": 300, "icon": "mdi:security"}
+ )
+ hass.states.async_set(
+ "light.kitchen", STATE_ON, {"brightness": 400, "icon": "mdi:security"}
+ )
+ hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"})
+
+ await _async_commit_and_wait(hass)
+
+ client = await hass_client()
+ response_json = await _async_fetch_logbook(client)
+
+ assert len(response_json) == 3
+ assert response_json[0]["domain"] == "homeassistant"
+ assert response_json[1]["entity_id"] == "light.kitchen"
+ assert response_json[1]["icon"] == "mdi:security"
+ assert response_json[1]["state"] == STATE_ON
+ assert response_json[2]["entity_id"] == "light.kitchen"
+ assert response_json[2]["icon"] == "mdi:chemical-weapon"
+ assert response_json[2]["state"] == STATE_OFF
+
+
+async def test_exclude_events_domain(hass, hass_client):
+ """Test if events are filtered if domain is excluded in config."""
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}},
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+
+ await _async_commit_and_wait(hass)
+
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 2
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+
+
+async def test_exclude_events_domain_glob(hass, hass_client):
+ """Test if events are filtered if domain or glob is excluded in config."""
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+ entity_id3 = "sensor.excluded"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_EXCLUDE: {
+ CONF_DOMAINS: ["switch", "alexa"],
+ CONF_ENTITY_GLOBS: "*.excluded",
+ }
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+ hass.states.async_set(entity_id3, None)
+ hass.states.async_set(entity_id3, 30)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 2
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+
+
+async def test_include_events_entity(hass, hass_client):
+ """Test if events are filtered if entity is included in config."""
+ entity_id = "sensor.bla"
+ entity_id2 = "sensor.blu"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_INCLUDE: {
+ CONF_DOMAINS: ["homeassistant"],
+ CONF_ENTITIES: [entity_id2],
+ }
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 2
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+
+
+async def test_exclude_events_entity(hass, hass_client):
+ """Test if events are filtered if entity is excluded in config."""
+ entity_id = "sensor.bla"
+ entity_id2 = "sensor.blu"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}},
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+ assert len(entries) == 2
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+
+
+async def test_include_events_domain(hass, hass_client):
+ """Test if events are filtered if domain is included in config."""
+ assert await async_setup_component(hass, "alexa", {})
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]}
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.bus.async_fire(
+ EVENT_ALEXA_SMART_HOME,
+ {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
+ )
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 3
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
+ _assert_entry(entries[2], name="blu", entity_id=entity_id2)
+
+
+async def test_include_events_domain_glob(hass, hass_client):
+ """Test if events are filtered if domain or glob is included in config."""
+ assert await async_setup_component(hass, "alexa", {})
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+ entity_id3 = "switch.included"
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_INCLUDE: {
+ CONF_DOMAINS: ["homeassistant", "sensor", "alexa"],
+ CONF_ENTITY_GLOBS: ["*.included"],
+ }
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.bus.async_fire(
+ EVENT_ALEXA_SMART_HOME,
+ {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
+ )
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 20)
+ hass.states.async_set(entity_id3, None)
+ hass.states.async_set(entity_id3, 30)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 4
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
+ _assert_entry(entries[2], name="blu", entity_id=entity_id2)
+ _assert_entry(entries[3], name="included", entity_id=entity_id3)
+
+
+async def test_include_exclude_events(hass, hass_client):
+ """Test if events are filtered if include and exclude is configured."""
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+ entity_id3 = "sensor.bli"
+ entity_id4 = "sensor.keep"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_INCLUDE: {
+ CONF_DOMAINS: ["sensor", "homeassistant"],
+ CONF_ENTITIES: ["switch.bla"],
+ },
+ CONF_EXCLUDE: {
+ CONF_DOMAINS: ["switch"],
+ CONF_ENTITIES: ["sensor.bli"],
+ },
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 10)
+ hass.states.async_set(entity_id3, None)
+ hass.states.async_set(entity_id3, 10)
+ hass.states.async_set(entity_id, 20)
+ hass.states.async_set(entity_id2, 20)
+ hass.states.async_set(entity_id4, None)
+ hass.states.async_set(entity_id4, 10)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 3
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+ _assert_entry(entries[2], name="keep", entity_id=entity_id4)
+
+
+async def test_include_exclude_events_with_glob_filters(hass, hass_client):
+ """Test if events are filtered if include and exclude is configured."""
+ entity_id = "switch.bla"
+ entity_id2 = "sensor.blu"
+ entity_id3 = "sensor.bli"
+ entity_id4 = "light.included"
+ entity_id5 = "switch.included"
+ entity_id6 = "sensor.excluded"
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {
+ CONF_INCLUDE: {
+ CONF_DOMAINS: ["sensor", "homeassistant"],
+ CONF_ENTITIES: ["switch.bla"],
+ CONF_ENTITY_GLOBS: ["*.included"],
+ },
+ CONF_EXCLUDE: {
+ CONF_DOMAINS: ["switch"],
+ CONF_ENTITY_GLOBS: ["*.excluded"],
+ CONF_ENTITIES: ["sensor.bli"],
+ },
+ },
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+ hass.states.async_set(entity_id2, None)
+ hass.states.async_set(entity_id2, 10)
+ hass.states.async_set(entity_id3, None)
+ hass.states.async_set(entity_id3, 10)
+ hass.states.async_set(entity_id, 20)
+ hass.states.async_set(entity_id2, 20)
+ hass.states.async_set(entity_id4, None)
+ hass.states.async_set(entity_id4, 30)
+ hass.states.async_set(entity_id5, None)
+ hass.states.async_set(entity_id5, 30)
+ hass.states.async_set(entity_id6, None)
+ hass.states.async_set(entity_id6, 30)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 3
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id2)
+ _assert_entry(entries[2], name="included", entity_id=entity_id4)
+
+
+async def test_empty_config(hass, hass_client):
+ """Test we can handle an empty entity filter."""
+ entity_id = "sensor.blu"
+
+ config = logbook.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ logbook.DOMAIN: {},
+ }
+ )
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ await async_setup_component(hass, "logbook", config)
+ await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, 10)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+ entries = await _async_fetch_logbook(client)
+
+ assert len(entries) == 2
+ _assert_entry(
+ entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
+ )
+ _assert_entry(entries[1], name="blu", entity_id=entity_id)
+
+
+async def _async_fetch_logbook(client):
+
+ # Today time 00:00:00
+ start = dt_util.utcnow().date()
+ start_date = datetime(start.year, start.month, start.day) - timedelta(hours=24)
+
+ # Test today entries without filters
+ end_time = start + timedelta(hours=48)
+ response = await client.get(
+ f"/api/logbook/{start_date.isoformat()}?end_time={end_time}"
+ )
+ assert response.status == 200
+ return await response.json()
+
+
+async def _async_commit_and_wait(hass):
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(trigger_db_commit, hass)
+ await hass.async_block_till_done()
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+ await hass.async_block_till_done()
+
+
+def _assert_entry(
+ entry, when=None, name=None, message=None, domain=None, entity_id=None
+):
+ """Assert an entry is what is expected."""
+ if when:
+ assert when.isoformat() == entry["when"]
+
+ if name:
+ assert name == entry["name"]
+
+ if message:
+ assert message == entry["message"]
+
+ if domain:
+ assert domain == entry["domain"]
+
+ if entity_id:
+ assert entity_id == entry["entity_id"]
+
+
class MockLazyEventPartialState(ha.Event):
"""Minimal mock of a Lazy event."""
diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py
index ee2c9be377f90d..ba0072bc2f8a81 100644
--- a/tests/components/media_player/test_reproduce_state.py
+++ b/tests/components/media_player/test_reproduce_state.py
@@ -7,7 +7,6 @@
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_ENQUEUE,
- ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_SOUND_MODE,
@@ -20,7 +19,6 @@
from homeassistant.const import (
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
- SERVICE_MEDIA_SEEK,
SERVICE_MEDIA_STOP,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -53,6 +51,8 @@
async def test_state(hass, service, state):
"""Test that we can turn a state into a service call."""
calls_1 = async_mock_service(hass, DOMAIN, service)
+ if service != SERVICE_TURN_ON:
+ async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
await async_reproduce_states(hass, [State(ENTITY_1, state)])
@@ -149,7 +149,6 @@ async def test_attribute_no_state(hass):
[
(SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL),
(SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED),
- (SERVICE_MEDIA_SEEK, ATTR_MEDIA_SEEK_POSITION),
(SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE),
(SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE),
],
diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py
index 68e0fcda1d8a36..a891fb0d11dc3f 100644
--- a/tests/components/media_source/test_init.py
+++ b/tests/components/media_source/test_init.py
@@ -5,6 +5,7 @@
from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source import const
+from homeassistant.components.media_source.error import Unresolvable
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
@@ -62,11 +63,23 @@ async def test_async_resolve_media(hass):
assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
- # Test no media content
- media = await media_source.async_resolve_media(hass, "")
+ media = await media_source.async_resolve_media(
+ hass,
+ media_source.generate_media_source_id(const.DOMAIN, "local/test.mp3"),
+ )
assert isinstance(media, media_source.models.PlayMedia)
+async def test_async_unresolve_media(hass):
+ """Test browse media."""
+ assert await async_setup_component(hass, const.DOMAIN, {})
+ await hass.async_block_till_done()
+
+ # Test no media content
+ with pytest.raises(Unresolvable):
+ await media_source.async_resolve_media(hass, "")
+
+
async def test_websocket_browse_media(hass, hass_ws_client):
"""Test browse media websocket."""
assert await async_setup_component(hass, const.DOMAIN, {})
@@ -127,7 +140,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
client = await hass_ws_client(hass)
- media = media_source.models.PlayMedia("/media/test.mp3", "audio/mpeg")
+ media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg")
with patch(
"homeassistant.components.media_source.async_resolve_media",
@@ -137,7 +150,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
{
"id": 1,
"type": "media_source/resolve_media",
- "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3",
+ "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3",
}
)
diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py
index 44d3810794959d..e3e2a3f1617d61 100644
--- a/tests/components/media_source/test_local_source.py
+++ b/tests/components/media_source/test_local_source.py
@@ -3,25 +3,32 @@
from homeassistant.components import media_source
from homeassistant.components.media_source import const
+from homeassistant.config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
async def test_async_browse_media(hass):
"""Test browse media."""
+ local_media = hass.config.path("media")
+ await async_process_ha_core_config(
+ hass, {"media_dirs": {"local": local_media, "recordings": local_media}}
+ )
+ await hass.async_block_till_done()
+
assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
# Test path not exists
with pytest.raises(media_source.BrowseError) as excinfo:
await media_source.async_browse_media(
- hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test/not/exist"
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist"
)
assert str(excinfo.value) == "Path does not exist."
# Test browse file
with pytest.raises(media_source.BrowseError) as excinfo:
await media_source.async_browse_media(
- hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3"
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3"
)
assert str(excinfo.value) == "Path is not a directory."
@@ -35,32 +42,58 @@ async def test_async_browse_media(hass):
# Test directory traversal
with pytest.raises(media_source.BrowseError) as excinfo:
await media_source.async_browse_media(
- hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/../configuration.yaml"
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/../configuration.yaml"
)
assert str(excinfo.value) == "Invalid path."
# Test successful listing
media = await media_source.async_browse_media(
- hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/."
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}"
+ )
+ assert media
+
+ media = await media_source.async_browse_media(
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/."
+ )
+ assert media
+
+ media = await media_source.async_browse_media(
+ hass, f"{const.URI_SCHEME}{const.DOMAIN}/recordings/."
)
assert media
async def test_media_view(hass, hass_client):
"""Test media view."""
+ local_media = hass.config.path("media")
+ await async_process_ha_core_config(
+ hass, {"media_dirs": {"local": local_media, "recordings": local_media}}
+ )
+ await hass.async_block_till_done()
+
assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
client = await hass_client()
# Protects against non-existent files
- resp = await client.get("/media/invalid.txt")
+ resp = await client.get("/media/local/invalid.txt")
+ assert resp.status == 404
+
+ resp = await client.get("/media/recordings/invalid.txt")
assert resp.status == 404
# Protects against non-media files
- resp = await client.get("/media/not_media.txt")
+ resp = await client.get("/media/local/not_media.txt")
+ assert resp.status == 404
+
+ # Protects against unknown local media sources
+ resp = await client.get("/media/unknown_source/not_media.txt")
assert resp.status == 404
# Fetch available media
- resp = await client.get("/media/test.mp3")
+ resp = await client.get("/media/local/test.mp3")
+ assert resp.status == 200
+
+ resp = await client.get("/media/recordings/test.mp3")
assert resp.status == 200
diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py
index 4d820c1c7c684a..52959428917d69 100644
--- a/tests/components/met/test_config_flow.py
+++ b/tests/components/met/test_config_flow.py
@@ -91,7 +91,7 @@ async def test_flow_entry_already_exists(hass):
)
assert result["type"] == "form"
- assert result["errors"]["name"] == "name_exists"
+ assert result["errors"]["name"] == "already_configured"
async def test_onboarding_step(hass):
diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py
index b47bb941af4370..ad3a27d724aefd 100644
--- a/tests/components/mikrotik/test_config_flow.py
+++ b/tests/components/mikrotik/test_config_flow.py
@@ -203,6 +203,6 @@ async def test_wrong_credentials(hass, auth_error):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {
- CONF_USERNAME: "wrong_credentials",
- CONF_PASSWORD: "wrong_credentials",
+ CONF_USERNAME: "invalid_auth",
+ CONF_PASSWORD: "invalid_auth",
}
diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py
index 66de0d47b532c8..0d3a2e8fdcd8ee 100644
--- a/tests/components/min_max/test_sensor.py
+++ b/tests/components/min_max/test_sensor.py
@@ -1,7 +1,6 @@
"""The test for the min/max sensor platform."""
from os import path
import statistics
-import unittest
from homeassistant import config as hass_config
from homeassistant.components.min_max import DOMAIN
@@ -14,329 +13,334 @@
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
-from homeassistant.setup import async_setup_component, setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import get_test_home_assistant
-
-
-class TestMinMaxSensor(unittest.TestCase):
- """Test the min/max sensor."""
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.values = [17, 20, 15.3]
- self.count = len(self.values)
- self.min = min(self.values)
- self.max = max(self.values)
- self.mean = round(sum(self.values) / self.count, 2)
- self.mean_1_digit = round(sum(self.values) / self.count, 1)
- self.mean_4_digits = round(sum(self.values) / self.count, 4)
- self.median = round(statistics.median(self.values), 2)
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_min_sensor(self):
- """Test the min sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_min",
- "type": "min",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+
+VALUES = [17, 20, 15.3]
+COUNT = len(VALUES)
+MIN_VALUE = min(VALUES)
+MAX_VALUE = max(VALUES)
+MEAN = round(sum(VALUES) / COUNT, 2)
+MEAN_1_DIGIT = round(sum(VALUES) / COUNT, 1)
+MEAN_4_DIGITS = round(sum(VALUES) / COUNT, 4)
+MEDIAN = round(statistics.median(VALUES), 2)
+
+
+async def test_min_sensor(hass):
+ """Test the min sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_min",
+ "type": "min",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
+
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "sensor", config)
+ entity_ids = config["sensor"]["entity_ids"]
- entity_ids = config["sensor"]["entity_ids"]
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ state = hass.states.get("sensor.test_min")
- state = self.hass.states.get("sensor.test_min")
+ assert str(float(MIN_VALUE)) == state.state
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEAN == state.attributes.get("mean")
+ assert MEDIAN == state.attributes.get("median")
- assert str(float(self.min)) == state.state
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.max == state.attributes.get("max_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.mean == state.attributes.get("mean")
- assert self.median == state.attributes.get("median")
- def test_max_sensor(self):
- """Test the max sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_max",
- "type": "max",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+async def test_max_sensor(hass):
+ """Test the max sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_max",
+ "type": "max",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
- assert setup_component(self.hass, "sensor", config)
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- entity_ids = config["sensor"]["entity_ids"]
+ entity_ids = config["sensor"]["entity_ids"]
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test_max")
+ state = hass.states.get("sensor.test_max")
- assert str(float(self.max)) == state.state
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.min == state.attributes.get("min_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.mean == state.attributes.get("mean")
- assert self.median == state.attributes.get("median")
+ assert str(float(MAX_VALUE)) == state.state
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEAN == state.attributes.get("mean")
+ assert MEDIAN == state.attributes.get("median")
- def test_mean_sensor(self):
- """Test the mean sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_mean",
- "type": "mean",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+
+async def test_mean_sensor(hass):
+ """Test the mean sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_mean",
+ "type": "mean",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
+
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "sensor", config)
+ entity_ids = config["sensor"]["entity_ids"]
- entity_ids = config["sensor"]["entity_ids"]
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ state = hass.states.get("sensor.test_mean")
+
+ assert str(float(MEAN)) == state.state
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEDIAN == state.attributes.get("median")
+
+
+async def test_mean_1_digit_sensor(hass):
+ """Test the mean with 1-digit precision sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_mean",
+ "type": "mean",
+ "round_digits": 1,
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
+ }
+ }
- state = self.hass.states.get("sensor.test_mean")
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- assert str(float(self.mean)) == state.state
- assert self.min == state.attributes.get("min_value")
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.max == state.attributes.get("max_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.median == state.attributes.get("median")
+ entity_ids = config["sensor"]["entity_ids"]
- def test_mean_1_digit_sensor(self):
- """Test the mean with 1-digit precision sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_mean",
- "type": "mean",
- "round_digits": 1,
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_mean")
+
+ assert str(float(MEAN_1_DIGIT)) == state.state
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEDIAN == state.attributes.get("median")
+
+
+async def test_mean_4_digit_sensor(hass):
+ """Test the mean with 1-digit precision sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_mean",
+ "type": "mean",
+ "round_digits": 4,
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
- assert setup_component(self.hass, "sensor", config)
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- entity_ids = config["sensor"]["entity_ids"]
+ entity_ids = config["sensor"]["entity_ids"]
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test_mean")
+ state = hass.states.get("sensor.test_mean")
- assert str(float(self.mean_1_digit)) == state.state
- assert self.min == state.attributes.get("min_value")
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.max == state.attributes.get("max_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.median == state.attributes.get("median")
+ assert str(float(MEAN_4_DIGITS)) == state.state
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEDIAN == state.attributes.get("median")
- def test_mean_4_digit_sensor(self):
- """Test the mean with 1-digit precision sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_mean",
- "type": "mean",
- "round_digits": 4,
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+
+async def test_median_sensor(hass):
+ """Test the median sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_median",
+ "type": "median",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
- assert setup_component(self.hass, "sensor", config)
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- entity_ids = config["sensor"]["entity_ids"]
+ entity_ids = config["sensor"]["entity_ids"]
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test_mean")
+ state = hass.states.get("sensor.test_median")
- assert str(float(self.mean_4_digits)) == state.state
- assert self.min == state.attributes.get("min_value")
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.max == state.attributes.get("max_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.median == state.attributes.get("median")
+ assert str(float(MEDIAN)) == state.state
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert entity_ids[2] == state.attributes.get("min_entity_id")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert MEAN == state.attributes.get("mean")
- def test_median_sensor(self):
- """Test the median sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_median",
- "type": "median",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+
+async def test_not_enough_sensor_value(hass):
+ """Test that there is nothing done if not enough values available."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_max",
+ "type": "max",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
- assert setup_component(self.hass, "sensor", config)
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- entity_ids = config["sensor"]["entity_ids"]
+ entity_ids = config["sensor"]["entity_ids"]
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
+ hass.states.async_set(entity_ids[0], STATE_UNKNOWN)
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test_median")
+ state = hass.states.get("sensor.test_max")
+ assert STATE_UNKNOWN == state.state
+ assert state.attributes.get("min_entity_id") is None
+ assert state.attributes.get("min_value") is None
+ assert state.attributes.get("max_entity_id") is None
+ assert state.attributes.get("max_value") is None
+ assert state.attributes.get("median") is None
- assert str(float(self.median)) == state.state
- assert self.min == state.attributes.get("min_value")
- assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert self.max == state.attributes.get("max_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.mean == state.attributes.get("mean")
+ hass.states.async_set(entity_ids[1], VALUES[1])
+ await hass.async_block_till_done()
- def test_not_enough_sensor_value(self):
- """Test that there is nothing done if not enough values available."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_max",
- "type": "max",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
- }
+ state = hass.states.get("sensor.test_max")
+ assert STATE_UNKNOWN != state.state
+ assert entity_ids[1] == state.attributes.get("min_entity_id")
+ assert VALUES[1] == state.attributes.get("min_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert VALUES[1] == state.attributes.get("max_value")
- assert setup_component(self.hass, "sensor", config)
-
- entity_ids = config["sensor"]["entity_ids"]
-
- self.hass.states.set(entity_ids[0], STATE_UNKNOWN)
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN == state.state
- assert state.attributes.get("min_entity_id") is None
- assert state.attributes.get("min_value") is None
- assert state.attributes.get("max_entity_id") is None
- assert state.attributes.get("max_value") is None
- assert state.attributes.get("median") is None
-
- self.hass.states.set(entity_ids[1], self.values[1])
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN != state.state
- assert entity_ids[1] == state.attributes.get("min_entity_id")
- assert self.values[1] == state.attributes.get("min_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.values[1] == state.attributes.get("max_value")
-
- self.hass.states.set(entity_ids[2], STATE_UNKNOWN)
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN != state.state
- assert entity_ids[1] == state.attributes.get("min_entity_id")
- assert self.values[1] == state.attributes.get("min_value")
- assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert self.values[1] == state.attributes.get("max_value")
-
- self.hass.states.set(entity_ids[1], STATE_UNAVAILABLE)
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN == state.state
- assert state.attributes.get("min_entity_id") is None
- assert state.attributes.get("min_value") is None
- assert state.attributes.get("max_entity_id") is None
- assert state.attributes.get("max_value") is None
-
- def test_different_unit_of_measurement(self):
- """Test for different unit of measurement."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test",
- "type": "mean",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
+ hass.states.async_set(entity_ids[2], STATE_UNKNOWN)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_max")
+ assert STATE_UNKNOWN != state.state
+ assert entity_ids[1] == state.attributes.get("min_entity_id")
+ assert VALUES[1] == state.attributes.get("min_value")
+ assert entity_ids[1] == state.attributes.get("max_entity_id")
+ assert VALUES[1] == state.attributes.get("max_value")
+
+ hass.states.async_set(entity_ids[1], STATE_UNAVAILABLE)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_max")
+ assert STATE_UNKNOWN == state.state
+ assert state.attributes.get("min_entity_id") is None
+ assert state.attributes.get("min_value") is None
+ assert state.attributes.get("max_entity_id") is None
+ assert state.attributes.get("max_value") is None
+
+
+async def test_different_unit_of_measurement(hass):
+ """Test for different unit of measurement."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test",
+ "type": "mean",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
}
+ }
- assert setup_component(self.hass, "sensor", config)
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- entity_ids = config["sensor"]["entity_ids"]
+ entity_ids = config["sensor"]["entity_ids"]
- self.hass.states.set(
- entity_ids[0], self.values[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
- )
- self.hass.block_till_done()
+ hass.states.async_set(
+ entity_ids[0], VALUES[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test")
+ state = hass.states.get("sensor.test")
- assert str(float(self.values[0])) == state.state
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert str(float(VALUES[0])) == state.state
+ assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
- self.hass.states.set(
- entity_ids[1], self.values[1], {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}
- )
- self.hass.block_till_done()
+ hass.states.async_set(
+ entity_ids[1], VALUES[1], {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test")
+ state = hass.states.get("sensor.test")
- assert STATE_UNKNOWN == state.state
- assert state.attributes.get("unit_of_measurement") == "ERR"
+ assert STATE_UNKNOWN == state.state
+ assert state.attributes.get("unit_of_measurement") == "ERR"
- self.hass.states.set(
- entity_ids[2], self.values[2], {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}
- )
- self.hass.block_till_done()
+ hass.states.async_set(
+ entity_ids[2], VALUES[2], {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test")
+ state = hass.states.get("sensor.test")
- assert STATE_UNKNOWN == state.state
- assert state.attributes.get("unit_of_measurement") == "ERR"
+ assert STATE_UNKNOWN == state.state
+ assert state.attributes.get("unit_of_measurement") == "ERR"
- def test_last_sensor(self):
- """Test the last sensor."""
- config = {
- "sensor": {
- "platform": "min_max",
- "name": "test_last",
- "type": "last",
- "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
- }
- }
- assert setup_component(self.hass, "sensor", config)
+async def test_last_sensor(hass):
+ """Test the last sensor."""
+ config = {
+ "sensor": {
+ "platform": "min_max",
+ "name": "test_last",
+ "type": "last",
+ "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
+ }
+ }
- entity_ids = config["sensor"]["entity_ids"]
- state = self.hass.states.get("sensor.test_last")
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- for entity_id, value in dict(zip(entity_ids, self.values)).items():
- self.hass.states.set(entity_id, value)
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_last")
- assert str(float(value)) == state.state
- assert entity_id == state.attributes.get("last_entity_id")
+ entity_ids = config["sensor"]["entity_ids"]
- assert self.min == state.attributes.get("min_value")
- assert self.max == state.attributes.get("max_value")
- assert self.mean == state.attributes.get("mean")
- assert self.median == state.attributes.get("median")
+ for entity_id, value in dict(zip(entity_ids, VALUES)).items():
+ hass.states.async_set(entity_id, value)
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_last")
+ assert str(float(value)) == state.state
+ assert entity_id == state.attributes.get("last_entity_id")
+
+ assert MIN_VALUE == state.attributes.get("min_value")
+ assert MAX_VALUE == state.attributes.get("max_value")
+ assert MEAN == state.attributes.get("mean")
+ assert MEDIAN == state.attributes.get("median")
async def test_reload(hass):
diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py
index 885aa5fc235a58..36495acc9c5d11 100644
--- a/tests/components/modbus/conftest.py
+++ b/tests/components/modbus/conftest.py
@@ -1,19 +1,17 @@
"""The tests for the Modbus sensor component."""
-from datetime import timedelta
import logging
from unittest import mock
import pytest
from homeassistant.components.modbus.const import (
+ CALL_TYPE_COIL,
+ CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_INPUT,
- CONF_REGISTER,
- CONF_REGISTER_TYPE,
- CONF_REGISTERS,
DEFAULT_HUB,
MODBUS_DOMAIN as DOMAIN,
)
-from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL
+from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -38,46 +36,66 @@ class ReadResult:
def __init__(self, register_words):
"""Init."""
self.registers = register_words
+ self.bits = register_words
-async def run_test(
- hass, use_mock_hub, register_config, entity_domain, register_words, expected
+async def setup_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ data_array,
+ entity_domain,
+ scan_interval,
):
- """Run test for given config and check that sensor outputs expected result."""
+ """Run setup device for given config."""
# Full sensor configuration
- sensor_name = "modbus_test_sensor"
- scan_interval = 5
config = {
entity_domain: {
CONF_PLATFORM: "modbus",
CONF_SCAN_INTERVAL: scan_interval,
- CONF_REGISTERS: [
- dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config)
- ],
+ **data_array,
}
}
- # Setup inputs for the sensor
- read_result = ReadResult(register_words)
- if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT:
- use_mock_hub.read_input_registers.return_value = read_result
- else:
- use_mock_hub.read_holding_registers.return_value = read_result
-
# Initialize sensor
now = dt_util.utcnow()
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
assert await async_setup_component(hass, entity_domain, config)
await hass.async_block_till_done()
+ entity_id = f"{entity_domain}.{sensor_name}"
+ device = hass.states.get(entity_id)
+ return entity_id, now, device
+
+
+async def run_base_read_test(
+ entity_id,
+ hass,
+ use_mock_hub,
+ register_type,
+ register_words,
+ expected,
+ now,
+):
+ """Run test for given config."""
+
+ # Setup inputs for the sensor
+ read_result = ReadResult(register_words)
+ if register_type == CALL_TYPE_COIL:
+ use_mock_hub.read_coils.return_value = read_result
+ elif register_type == CALL_TYPE_DISCRETE:
+ use_mock_hub.read_discrete_inputs.return_value = read_result
+ elif register_type == CALL_TYPE_REGISTER_INPUT:
+ use_mock_hub.read_input_registers.return_value = read_result
+ else: # CALL_TYPE_REGISTER_HOLDING
+ use_mock_hub.read_holding_registers.return_value = read_result
+
# Trigger update call with time_changed event
- now += timedelta(seconds=scan_interval + 1)
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Check state
- entity_id = f"{entity_domain}.{sensor_name}"
state = hass.states.get(entity_id).state
assert state == expected
diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py
new file mode 100644
index 00000000000000..63513872e79fe5
--- /dev/null
+++ b/tests/components/modbus/test_modbus_binary_sensor.py
@@ -0,0 +1,100 @@
+"""The tests for the Modbus sensor component."""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.modbus.const import (
+ CALL_TYPE_COIL,
+ CALL_TYPE_DISCRETE,
+ CONF_ADDRESS,
+ CONF_INPUT_TYPE,
+ CONF_INPUTS,
+)
+from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
+
+from .conftest import run_base_read_test, setup_base_test
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def run_sensor_test(hass, use_mock_hub, register_config, value, expected):
+ """Run test for given config."""
+ sensor_name = "modbus_test_binary_sensor"
+ scan_interval = 5
+ entity_id, now, device = await setup_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ {
+ CONF_INPUTS: [
+ dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **register_config)
+ ]
+ },
+ SENSOR_DOMAIN,
+ scan_interval,
+ )
+ await run_base_read_test(
+ entity_id,
+ hass,
+ use_mock_hub,
+ register_config.get(CONF_INPUT_TYPE),
+ value,
+ expected,
+ now + timedelta(seconds=scan_interval + 1),
+ )
+
+
+async def test_coil_true(hass, mock_hub):
+ """Test conversion of single word register."""
+ register_config = {
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ }
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ register_config,
+ [0xFF],
+ STATE_ON,
+ )
+
+
+async def test_coil_false(hass, mock_hub):
+ """Test conversion of single word register."""
+ register_config = {
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ }
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ register_config,
+ [0x00],
+ STATE_OFF,
+ )
+
+
+async def test_discrete_true(hass, mock_hub):
+ """Test conversion of single word register."""
+ register_config = {
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ }
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ register_config,
+ [0xFF],
+ expected="on",
+ )
+
+
+async def test_discrete_false(hass, mock_hub):
+ """Test conversion of single word register."""
+ register_config = {
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ }
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ register_config,
+ [0x00],
+ expected="off",
+ )
diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py
index ab4d745dc50a0f..59d845ef053bf0 100644
--- a/tests/components/modbus/test_modbus_sensor.py
+++ b/tests/components/modbus/test_modbus_sensor.py
@@ -1,4 +1,5 @@
"""The tests for the Modbus sensor component."""
+from datetime import timedelta
import logging
from homeassistant.components.modbus.const import (
@@ -8,7 +9,9 @@
CONF_DATA_TYPE,
CONF_OFFSET,
CONF_PRECISION,
+ CONF_REGISTER,
CONF_REGISTER_TYPE,
+ CONF_REGISTERS,
CONF_REVERSE_ORDER,
CONF_SCALE,
DATA_TYPE_FLOAT,
@@ -17,364 +20,356 @@
DATA_TYPE_UINT,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import CONF_NAME
-from .conftest import run_test
+from .conftest import run_base_read_test, setup_base_test
_LOGGER = logging.getLogger(__name__)
+async def run_sensor_test(
+ hass, use_mock_hub, register_config, register_words, expected
+):
+ """Run test for sensor."""
+ sensor_name = "modbus_test_sensor"
+ scan_interval = 5
+ entity_id, now, device = await setup_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ {
+ CONF_REGISTERS: [
+ dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config)
+ ]
+ },
+ SENSOR_DOMAIN,
+ scan_interval,
+ )
+ await run_base_read_test(
+ entity_id,
+ hass,
+ use_mock_hub,
+ register_config.get(CONF_REGISTER_TYPE),
+ register_words,
+ expected,
+ now + timedelta(seconds=scan_interval + 1),
+ )
+
+
async def test_simple_word_register(hass, mock_hub):
"""Test conversion of single word register."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0],
- expected="0",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0],
+ "0",
)
async def test_optional_conf_keys(hass, mock_hub):
"""Test handling of optional configuration keys."""
- register_config = {}
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x8000],
- expected="-32768",
+ {},
+ [0x8000],
+ "-32768",
)
async def test_offset(hass, mock_hub):
"""Test offset calculation."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 1,
- CONF_OFFSET: 13,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[7],
- expected="20",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 13,
+ CONF_PRECISION: 0,
+ },
+ [7],
+ "20",
)
async def test_scale_and_offset(hass, mock_hub):
"""Test handling of scale and offset."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 3,
- CONF_OFFSET: 13,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[7],
- expected="34",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 3,
+ CONF_OFFSET: 13,
+ CONF_PRECISION: 0,
+ },
+ [7],
+ "34",
)
async def test_ints_can_have_precision(hass, mock_hub):
"""Test precision can be specified event if using integer values only."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 3,
- CONF_OFFSET: 13,
- CONF_PRECISION: 4,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[7],
- expected="34.0000",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 3,
+ CONF_OFFSET: 13,
+ CONF_PRECISION: 4,
+ },
+ [7],
+ "34.0000",
)
async def test_floats_get_rounded_correctly(hass, mock_hub):
"""Test that floating point values get rounded correctly."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 1.5,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[1],
- expected="2",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 1.5,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [1],
+ "2",
)
async def test_parameters_as_strings(hass, mock_hub):
"""Test that scale, offset and precision can be given as strings."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: "1.5",
- CONF_OFFSET: "5",
- CONF_PRECISION: "1",
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[9],
- expected="18.5",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: "1.5",
+ CONF_OFFSET: "5",
+ CONF_PRECISION: "1",
+ },
+ [9],
+ "18.5",
)
async def test_floating_point_scale(hass, mock_hub):
"""Test use of floating point scale."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 2.4,
- CONF_OFFSET: 0,
- CONF_PRECISION: 2,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[1],
- expected="2.40",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 2.4,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 2,
+ },
+ [1],
+ "2.40",
)
async def test_floating_point_offset(hass, mock_hub):
"""Test use of floating point scale."""
- register_config = {
- CONF_COUNT: 1,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 1,
- CONF_OFFSET: -10.3,
- CONF_PRECISION: 1,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[2],
- expected="-8.3",
+ {
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: -10.3,
+ CONF_PRECISION: 1,
+ },
+ [2],
+ "-8.3",
)
async def test_signed_two_word_register(hass, mock_hub):
"""Test reading of signed register with two words."""
- register_config = {
- CONF_COUNT: 2,
- CONF_DATA_TYPE: DATA_TYPE_INT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF],
- expected="-1985229329",
+ {
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_INT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x89AB, 0xCDEF],
+ "-1985229329",
)
async def test_unsigned_two_word_register(hass, mock_hub):
"""Test reading of unsigned register with two words."""
- register_config = {
- CONF_COUNT: 2,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF],
- expected=str(0x89ABCDEF),
+ {
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x89AB, 0xCDEF],
+ str(0x89ABCDEF),
)
async def test_reversed(hass, mock_hub):
"""Test handling of reversed register words."""
- register_config = {
- CONF_COUNT: 2,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_REVERSE_ORDER: True,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF],
- expected=str(0xCDEF89AB),
+ {
+ CONF_COUNT: 2,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_REVERSE_ORDER: True,
+ },
+ [0x89AB, 0xCDEF],
+ str(0xCDEF89AB),
)
async def test_four_word_register(hass, mock_hub):
"""Test reading of 64-bit register."""
- register_config = {
- CONF_COUNT: 4,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567],
- expected="9920249030613615975",
+ {
+ CONF_COUNT: 4,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x89AB, 0xCDEF, 0x0123, 0x4567],
+ "9920249030613615975",
)
async def test_four_word_register_precision_is_intact_with_int_params(hass, mock_hub):
"""Test that precision is not lost when doing integer arithmetic for 64-bit register."""
- register_config = {
- CONF_COUNT: 4,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 2,
- CONF_OFFSET: 3,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF],
- expected="163971058432973793",
+ {
+ CONF_COUNT: 4,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 2,
+ CONF_OFFSET: 3,
+ CONF_PRECISION: 0,
+ },
+ [0x0123, 0x4567, 0x89AB, 0xCDEF],
+ "163971058432973793",
)
async def test_four_word_register_precision_is_lost_with_float_params(hass, mock_hub):
"""Test that precision is affected when floating point conversion is done."""
- register_config = {
- CONF_COUNT: 4,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 2.0,
- CONF_OFFSET: 3.0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF],
- expected="163971058432973792",
+ {
+ CONF_COUNT: 4,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 2.0,
+ CONF_OFFSET: 3.0,
+ CONF_PRECISION: 0,
+ },
+ [0x0123, 0x4567, 0x89AB, 0xCDEF],
+ "163971058432973792",
)
async def test_two_word_input_register(hass, mock_hub):
"""Test reaging of input register."""
- register_config = {
- CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF],
- expected=str(0x89ABCDEF),
+ {
+ CONF_COUNT: 2,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x89AB, 0xCDEF],
+ str(0x89ABCDEF),
)
async def test_two_word_holding_register(hass, mock_hub):
"""Test reaging of holding register."""
- register_config = {
- CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_DATA_TYPE: DATA_TYPE_UINT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x89AB, 0xCDEF],
- expected=str(0x89ABCDEF),
+ {
+ CONF_COUNT: 2,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DATA_TYPE: DATA_TYPE_UINT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x89AB, 0xCDEF],
+ str(0x89ABCDEF),
)
async def test_float_data_type(hass, mock_hub):
"""Test floating point register data type."""
- register_config = {
- CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_DATA_TYPE: DATA_TYPE_FLOAT,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 5,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[16286, 1617],
- expected="1.23457",
+ {
+ CONF_COUNT: 2,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DATA_TYPE: DATA_TYPE_FLOAT,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 5,
+ },
+ [16286, 1617],
+ "1.23457",
)
async def test_string_data_type(hass, mock_hub):
"""Test byte string register data type."""
- register_config = {
- CONF_COUNT: 8,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
- CONF_DATA_TYPE: DATA_TYPE_STRING,
- CONF_SCALE: 1,
- CONF_OFFSET: 0,
- CONF_PRECISION: 0,
- }
- await run_test(
+ await run_sensor_test(
hass,
mock_hub,
- register_config,
- SENSOR_DOMAIN,
- register_words=[0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335],
- expected="07-05-2020 14:35",
+ {
+ CONF_COUNT: 8,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DATA_TYPE: DATA_TYPE_STRING,
+ CONF_SCALE: 1,
+ CONF_OFFSET: 0,
+ CONF_PRECISION: 0,
+ },
+ [0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335],
+ "07-05-2020 14:35",
)
diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py
new file mode 100644
index 00000000000000..ac1d8bd696308a
--- /dev/null
+++ b/tests/components/modbus/test_modbus_switch.py
@@ -0,0 +1,59 @@
+"""The tests for the Modbus switch component."""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_COILS
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.const import CONF_NAME, CONF_SLAVE
+
+from .conftest import run_base_read_test, setup_base_test
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def run_sensor_test(hass, use_mock_hub, value, expected):
+ """Run test for given config."""
+ switch_name = "modbus_test_switch"
+ scan_interval = 5
+ entity_id, now, device = await setup_base_test(
+ switch_name,
+ hass,
+ use_mock_hub,
+ {
+ CONF_COILS: [
+ {CONF_NAME: switch_name, CALL_TYPE_COIL: 1234, CONF_SLAVE: 1},
+ ]
+ },
+ SWITCH_DOMAIN,
+ scan_interval,
+ )
+
+ await run_base_read_test(
+ entity_id,
+ hass,
+ use_mock_hub,
+ CALL_TYPE_COIL,
+ value,
+ expected,
+ now + timedelta(seconds=scan_interval + 1),
+ )
+
+
+async def test_read_coil_false(hass, mock_hub):
+ """Test reading of switch coil."""
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ [0x00],
+ expected="off",
+ )
+
+
+async def test_read_coil_true(hass, mock_hub):
+ """Test reading of switch coil."""
+ await run_sensor_test(
+ hass,
+ mock_hub,
+ [0xFF],
+ expected="on",
+ )
diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py
index d2375278c4d34f..2a718d60777252 100644
--- a/tests/components/mqtt/test_binary_sensor.py
+++ b/tests/components/mqtt/test_binary_sensor.py
@@ -53,9 +53,7 @@
}
-async def test_setting_sensor_value_expires_availability_topic(
- hass, mqtt_mock, legacy_patchable_time, caplog
-):
+async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
@@ -85,9 +83,7 @@ async def test_setting_sensor_value_expires_availability_topic(
await expires_helper(hass, mqtt_mock, caplog)
-async def test_setting_sensor_value_expires(
- hass, mqtt_mock, legacy_patchable_time, caplog
-):
+async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
@@ -113,7 +109,8 @@ async def test_setting_sensor_value_expires(
async def expires_helper(hass, mqtt_mock, caplog):
"""Run the basic expiry code."""
- now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC)
+ realnow = dt_util.utcnow()
+ now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC)
with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
async_fire_time_changed(hass, now)
async_fire_mqtt_message(hass, "test-topic", "ON")
@@ -161,6 +158,89 @@ async def expires_helper(hass, mqtt_mock, caplog):
assert state.state == STATE_UNAVAILABLE
+async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor(
+ hass, mqtt_mock, caplog
+):
+ """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update."""
+ entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
+ await async_start(hass, "homeassistant", entry)
+
+ config = {
+ "name": "Test",
+ "state_topic": "test-topic",
+ "expire_after": 4,
+ "force_update": True,
+ }
+
+ config_msg = json.dumps(config)
+
+ # Set time and publish config message to create binary_sensor via discovery with 4 s expiry
+ realnow = dt_util.utcnow()
+ now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC)
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ async_fire_mqtt_message(
+ hass, "homeassistant/binary_sensor/bla/config", config_msg
+ )
+ await hass.async_block_till_done()
+
+ # Test that binary_sensor is not available
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ # Publish state message
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_mqtt_message(hass, "test-topic", "ON")
+ await hass.async_block_till_done()
+
+ # Test that binary_sensor has correct state
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_ON
+
+ # Advance +3 seconds
+ now = now + timedelta(seconds=3)
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # binary_sensor is not yet expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_ON
+
+ # Resend config message to update discovery
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ async_fire_mqtt_message(
+ hass, "homeassistant/binary_sensor/bla/config", config_msg
+ )
+ await hass.async_block_till_done()
+
+ # Test that binary_sensor has not expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_ON
+
+ # Add +2 seconds
+ now = now + timedelta(seconds=2)
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # Test that binary_sensor has expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ # Resend config message to update discovery
+ with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
+ async_fire_mqtt_message(
+ hass, "homeassistant/binary_sensor/bla/config", config_msg
+ )
+ await hass.async_block_till_done()
+
+ # Test that binary_sensor is still expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_UNAVAILABLE
+
+
async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
"""Test the setting of the value via MQTT."""
assert await async_setup_component(
@@ -666,88 +746,6 @@ async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog)
)
-async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor(
- hass, mqtt_mock, legacy_patchable_time, caplog
-):
- """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update."""
- entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
- await async_start(hass, "homeassistant", entry)
-
- config = {
- "name": "Test",
- "state_topic": "test-topic",
- "expire_after": 4,
- "force_update": True,
- }
-
- config_msg = json.dumps(config)
-
- # Set time and publish config message to create binary_sensor via discovery with 4 s expiry
- now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC)
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_time_changed(hass, now)
- async_fire_mqtt_message(
- hass, "homeassistant/binary_sensor/bla/config", config_msg
- )
- await hass.async_block_till_done()
-
- # Test that binary_sensor is not available
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_UNAVAILABLE
-
- # Publish state message
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_mqtt_message(hass, "test-topic", "ON")
- await hass.async_block_till_done()
-
- # Test that binary_sensor has correct state
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_ON
-
- # Advance +3 seconds
- now = now + timedelta(seconds=3)
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_time_changed(hass, now)
- await hass.async_block_till_done()
-
- # binary_sensor is not yet expired
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_ON
-
- # Resend config message to update discovery
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_time_changed(hass, now)
- async_fire_mqtt_message(
- hass, "homeassistant/binary_sensor/bla/config", config_msg
- )
- await hass.async_block_till_done()
-
- # Test that binary_sensor has not expired
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_ON
-
- # Add +2 seconds
- now = now + timedelta(seconds=2)
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_time_changed(hass, now)
- await hass.async_block_till_done()
-
- # Test that binary_sensor has expired
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_UNAVAILABLE
-
- # Resend config message to update discovery
- with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now):
- async_fire_mqtt_message(
- hass, "homeassistant/binary_sensor/bla/config", config_msg
- )
- await hass.async_block_till_done()
-
- # Test that binary_sensor is still expired
- state = hass.states.get("binary_sensor.test")
- assert state.state == STATE_UNAVAILABLE
-
-
@pytest.mark.no_fail_on_log_exception
async def test_discovery_broken(hass, mqtt_mock, caplog):
"""Test handling of bad discovery message."""
diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py
index 77fd8c561b2e03..73c22b057265d4 100644
--- a/tests/components/mqtt/test_sensor.py
+++ b/tests/components/mqtt/test_sensor.py
@@ -75,9 +75,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
assert state.attributes.get("unit_of_measurement") == "fav unit"
-async def test_setting_sensor_value_expires_availability_topic(
- hass, mqtt_mock, legacy_patchable_time, caplog
-):
+async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
@@ -107,9 +105,7 @@ async def test_setting_sensor_value_expires_availability_topic(
await expires_helper(hass, mqtt_mock, caplog)
-async def test_setting_sensor_value_expires(
- hass, mqtt_mock, legacy_patchable_time, caplog
-):
+async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py
new file mode 100644
index 00000000000000..fa041fb58ad526
--- /dev/null
+++ b/tests/components/mqtt/test_tag.py
@@ -0,0 +1,744 @@
+"""The tests for MQTT tag scanner."""
+import copy
+import json
+
+import pytest
+
+from homeassistant.components.mqtt import DOMAIN
+from homeassistant.components.mqtt.discovery import async_start
+
+from tests.async_mock import ANY, patch
+from tests.common import (
+ async_fire_mqtt_message,
+ async_get_device_automations,
+ mock_device_registry,
+ mock_registry,
+)
+
+DEFAULT_CONFIG_DEVICE = {
+ "device": {"identifiers": ["0AFFD2"]},
+ "topic": "foobar/tag_scanned",
+}
+
+DEFAULT_CONFIG = {
+ "topic": "foobar/tag_scanned",
+}
+
+DEFAULT_CONFIG_JSON = {
+ "device": {"identifiers": ["0AFFD2"]},
+ "topic": "foobar/tag_scanned",
+ "value_template": "{{ value_json.PN532.UID }}",
+}
+
+DEFAULT_TAG_ID = "E9F35959"
+
+DEFAULT_TAG_SCAN = "E9F35959"
+
+DEFAULT_TAG_SCAN_JSON = (
+ '{"Time":"2020-09-28T17:02:10","PN532":{"UID":"E9F35959", "DATA":"ILOVETASMOTA"}}'
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def tag_mock():
+ """Fixture to mock tag."""
+ with patch("homeassistant.components.tag.async_scan_tag") as mock_tag:
+ yield mock_tag
+
+
+@pytest.mark.no_fail_on_log_exception
+async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_mock):
+ """Test bad discovery message."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ # Test sending bad data
+ data0 = '{ "device":{"identifiers":["0AFFD2"]}, "topics": "foobar/tag_scanned" }'
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data0)
+ await hass.async_block_till_done()
+ assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None
+
+ # Test sending correct data
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", json.dumps(config1))
+ await hass.async_block_till_done()
+
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_if_fires_on_mqtt_message_with_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning, with device."""
+ config = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_if_fires_on_mqtt_message_without_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning, without device."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+
+async def test_if_fires_on_mqtt_message_with_template(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning, with device."""
+ config = copy.deepcopy(DEFAULT_CONFIG_JSON)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock):
+ """Test strip whitespace from tag_id."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", "123456 ")
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, "123456", None)
+
+
+async def test_if_fires_on_mqtt_message_after_update_with_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning after update."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+ config2 = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+ config2["topic"] = "foobar/tag_scanned2"
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Update the tag scanner with different topic
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned2", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Update the tag scanner with same topic
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned2", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_if_fires_on_mqtt_message_after_update_without_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning after update."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG)
+ config2 = copy.deepcopy(DEFAULT_CONFIG)
+ config2["topic"] = "foobar/tag_scanned2"
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1))
+ await hass.async_block_till_done()
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+ # Update the tag scanner with different topic
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned2", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+ # Update the tag scanner with same topic
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned2", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+
+async def test_if_fires_on_mqtt_message_after_update_with_template(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning after update."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG_JSON)
+ config2 = copy.deepcopy(DEFAULT_CONFIG_JSON)
+ config2["value_template"] = "{{ value_json.RDM6300.UID }}"
+ tag_scan_2 = '{"Time":"2020-09-28T17:02:10","RDM6300":{"UID":"E9F35959", "DATA":"ILOVETASMOTA"}}'
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Update the tag scanner with different template
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", tag_scan_2)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Update the tag scanner with same template
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config2))
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", tag_scan_2)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock):
+ """Test subscription to topics without change."""
+ config = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ call_count = mqtt_mock.async_subscribe.call_count
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ assert mqtt_mock.async_subscribe.call_count == call_count
+
+
+async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning after removal."""
+ config = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Remove the tag scanner
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "")
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ # Rediscover the tag scanner
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+
+async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device(
+ hass, device_reg, mqtt_mock, tag_mock
+):
+ """Test tag scanning not firing after removal."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+ # Remove the tag scanner
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "")
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+ # Rediscover the tag scanner
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, None)
+
+
+async def test_not_fires_on_mqtt_message_after_remove_from_registry(
+ hass,
+ device_reg,
+ mqtt_mock,
+ tag_mock,
+):
+ """Test tag scanning after removal."""
+ config = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
+
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
+
+ # Remove the device
+ device_reg.async_remove_device(device_entry.id)
+ await hass.async_block_till_done()
+ tag_mock.reset_mock()
+
+ async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
+ await hass.async_block_till_done()
+ tag_mock.assert_not_called()
+
+
+async def test_entity_device_info_with_connection(hass, mqtt_mock):
+ """Test MQTT device registry integration."""
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps(
+ {
+ "topic": "test-topic",
+ "device": {
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ }
+ )
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")})
+ assert device is not None
+ assert device.connections == {("mac", "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == "Whatever"
+ assert device.name == "Beer"
+ assert device.model == "Glass"
+ assert device.sw_version == "0.1-beta"
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT device registry integration."""
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps(
+ {
+ "topic": "test-topic",
+ "device": {
+ "identifiers": ["helloworld"],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ }
+ )
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.identifiers == {("mqtt", "helloworld")}
+ assert device.manufacturer == "Whatever"
+ assert device.name == "Beer"
+ assert device.model == "Glass"
+ assert device.sw_version == "0.1-beta"
+
+
+async def test_entity_device_info_update(hass, mqtt_mock):
+ """Test device registry update."""
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ config = {
+ "topic": "test-topic",
+ "device": {
+ "identifiers": ["helloworld"],
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ }
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Beer"
+
+ config["device"]["name"] = "Milk"
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Milk"
+
+
+async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock):
+ """Test tag discovery topic is cleaned when device is removed from registry."""
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ config = {
+ "topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is created
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ device_reg.async_remove_device(device_entry.id)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is None
+
+ # Verify retained discovery topic has been cleared
+ mqtt_mock.async_publish.assert_called_once_with(
+ "homeassistant/tag/bla/config", "", 0, True
+ )
+
+
+async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock):
+ """Test removal from device registry when tag is removed."""
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ config = {
+ "topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data)
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is created
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is None
+
+
+async def test_cleanup_device_several_tags(
+ hass, device_reg, entity_reg, mqtt_mock, tag_mock
+):
+ """Test removal from device registry when the last tag is removed."""
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ config1 = {
+ "topic": "test-topic1",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ config2 = {
+ "topic": "test-topic2",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1))
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla2/config", json.dumps(config2))
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is created
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is not cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ # Fake tag scan.
+ async_fire_mqtt_message(hass, "test-topic1", "12345")
+ async_fire_mqtt_message(hass, "test-topic2", "23456")
+ await hass.async_block_till_done()
+ tag_mock.assert_called_once_with(ANY, "23456", device_entry.id)
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla2/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is None
+
+
+async def test_cleanup_device_with_entity_and_trigger_1(
+ hass, device_reg, entity_reg, mqtt_mock
+):
+ """Test removal from device registry for device with tag, entity and trigger.
+
+ Tag removed first, then trigger and entity.
+ """
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ config1 = {
+ "topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ config2 = {
+ "automation_type": "trigger",
+ "topic": "test-topic",
+ "type": "foo",
+ "subtype": "bar",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ config3 = {
+ "name": "test_binary_sensor",
+ "state_topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ "unique_id": "veryunique",
+ }
+
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+ data3 = json.dumps(config3)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1)
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla3/config", data3)
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is created
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is not cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "")
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla3/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is None
+
+
+async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock):
+ """Test removal from device registry for device with tag, entity and trigger.
+
+ Trigger and entity removed first, then tag.
+ """
+ config_entry = hass.config_entries.async_entries(DOMAIN)[0]
+ await async_start(hass, "homeassistant", config_entry)
+
+ config1 = {
+ "topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ config2 = {
+ "automation_type": "trigger",
+ "topic": "test-topic",
+ "type": "foo",
+ "subtype": "bar",
+ "device": {"identifiers": ["helloworld"]},
+ }
+
+ config3 = {
+ "name": "test_binary_sensor",
+ "state_topic": "test-topic",
+ "device": {"identifiers": ["helloworld"]},
+ "unique_id": "veryunique",
+ }
+
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+ data3 = json.dumps(config3)
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1)
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla3/config", data3)
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is created
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger
+
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "")
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla3/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is not cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is not None
+
+ async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "")
+ await hass.async_block_till_done()
+
+ # Verify device registry entry is cleared
+ device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
+ assert device_entry is None
diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py
index f0dd76ff1b4770..13e96c69adc1f6 100644
--- a/tests/components/mqtt/test_trigger.py
+++ b/tests/components/mqtt/test_trigger.py
@@ -4,10 +4,10 @@
import pytest
import homeassistant.components.automation as automation
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.setup import async_setup_component
from tests.common import async_fire_mqtt_message, async_mock_service, mock_component
-from tests.components.automation import common
@pytest.fixture
@@ -44,11 +44,15 @@ async def test_if_fires_on_topic_match(hass, calls):
async_fire_mqtt_message(hass, "test-topic", '{ "hello": "world" }')
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
assert 'mqtt - test-topic - { "hello": "world" } - world' == calls[0].data["some"]
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_mqtt_message(hass, "test-topic", "test_payload")
await hass.async_block_till_done()
assert len(calls) == 1
diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py
index be69e0853ad5a3..31c2cddd09d9ed 100644
--- a/tests/components/neato/test_config_flow.py
+++ b/tests/components/neato/test_config_flow.py
@@ -118,7 +118,7 @@ async def test_abort_on_invalid_credentials(hass):
}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "invalid_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
result = await flow.async_step_import(
{
@@ -128,7 +128,7 @@ async def test_abort_on_invalid_credentials(hass):
}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "invalid_credentials"
+ assert result["reason"] == "invalid_auth"
async def test_abort_on_unexpected_error(hass):
@@ -147,7 +147,7 @@ async def test_abort_on_unexpected_error(hass):
}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "unexpected_error"}
+ assert result["errors"] == {"base": "unknown"}
result = await flow.async_step_import(
{
@@ -157,4 +157,4 @@ async def test_abort_on_unexpected_error(hass):
}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "unexpected_error"
+ assert result["reason"] == "unknown"
diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py
index 0405317f03e916..fd36c57dfd1b09 100644
--- a/tests/components/netatmo/test_media_source.py
+++ b/tests/components/netatmo/test_media_source.py
@@ -1,4 +1,6 @@
"""Test Local Media Source."""
+import ast
+
import pytest
from homeassistant.components import media_source
@@ -7,6 +9,8 @@
from homeassistant.components.netatmo import DATA_CAMERAS, DATA_EVENTS, DOMAIN
from homeassistant.setup import async_setup_component
+from tests.common import load_fixture
+
async def test_async_browse_media(hass):
"""Test browse media."""
@@ -14,32 +18,14 @@ async def test_async_browse_media(hass):
# Prepare cached Netatmo event date
hass.data[DOMAIN] = {}
- hass.data[DOMAIN][DATA_EVENTS] = {
- "12:34:56:78:90:ab": {
- 1599152672: {
- "id": "12345",
- "time": 1599152672,
- "camera_id": "12:34:56:78:90:ab",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- "video_id": "98765",
- "video_status": "available",
- "message": "Paulus seen",
- "media_url": "http:///files/high/index.m3u8",
- },
- 1599152673: {
- "id": "12346",
- "time": 1599152673,
- "camera_id": "12:34:56:78:90:ab",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- "message": "Tobias seen",
- },
- }
+ hass.data[DOMAIN][DATA_EVENTS] = ast.literal_eval(
+ load_fixture("netatmo/events.txt")
+ )
+
+ hass.data[DOMAIN][DATA_CAMERAS] = {
+ "12:34:56:78:90:ab": "MyCamera",
+ "12:34:56:78:90:ac": "MyOutdoorCamera",
}
- hass.data[DOMAIN][DATA_CAMERAS] = {"12:34:56:78:90:ab": "MyCamera"}
assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py
index 52064d1a92b315..c7f15068d1deab 100644
--- a/tests/components/nightscout/__init__.py
+++ b/tests/components/nightscout/__init__.py
@@ -22,6 +22,11 @@
'{"status":"ok","name":"nightscout","version":"13.0.1","serverTime":"2020-08-05T18:14:02.032Z","serverTimeEpoch":1596651242032,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{},"extendedSettings":{},"authorized":null}'
)
)
+SERVER_STATUS_STATUS_ONLY = ServerStatus.new_from_json_dict(
+ json.loads(
+ '{"status":"ok","name":"nightscout","version":"14.0.4","serverTime":"2020-09-25T21:03:59.315Z","serverTimeEpoch":1601067839315,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{"units":"mg/dl","timeFormat":12,"nightMode":false,"editMode":true,"showRawbg":"never","customTitle":"Nightscout","theme":"default","alarmUrgentHigh":true,"alarmUrgentHighMins":[30,60,90,120],"alarmHigh":true,"alarmHighMins":[30,60,90,120],"alarmLow":true,"alarmLowMins":[15,30,45,60],"alarmUrgentLow":true,"alarmUrgentLowMins":[15,30,45],"alarmUrgentMins":[30,60,90,120],"alarmWarnMins":[30,60,90,120],"alarmTimeagoWarn":true,"alarmTimeagoWarnMins":15,"alarmTimeagoUrgent":true,"alarmTimeagoUrgentMins":30,"alarmPumpBatteryLow":false,"language":"en","scaleY":"log","showPlugins":"dbsize delta direction upbat","showForecast":"ar2","focusHours":3,"heartbeat":60,"baseURL":"","authDefaultRoles":"status-only","thresholds":{"bgHigh":260,"bgTargetTop":180,"bgTargetBottom":80,"bgLow":55},"insecureUseHttp":true,"secureHstsHeader":false,"secureHstsHeaderIncludeSubdomains":false,"secureHstsHeaderPreload":false,"secureCsp":false,"deNormalizeDates":false,"showClockDelta":false,"showClockLastTime":false,"bolusRenderOver":1,"frameUrl1":"","frameUrl2":"","frameUrl3":"","frameUrl4":"","frameUrl5":"","frameUrl6":"","frameUrl7":"","frameUrl8":"","frameName1":"","frameName2":"","frameName3":"","frameName4":"","frameName5":"","frameName6":"","frameName7":"","frameName8":"","DEFAULT_FEATURES":["bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize"],"alarmTypes":["predict"],"enable":["careportal","boluscalc","food","bwp","cage","sage","iage","iob","cob","basal","ar2","rawbg","pushover","bgi","pump","openaps","treatmentnotify","bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize","ar2"]},"extendedSettings":{"devicestatus":{"advanced":true,"days":1}},"authorized":null}'
+ )
+)
async def init_integration(hass) -> MockConfigEntry:
diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py
index a5f3315fbb173a..71983f1b29db9e 100644
--- a/tests/components/nightscout/test_config_flow.py
+++ b/tests/components/nightscout/test_config_flow.py
@@ -1,5 +1,5 @@
"""Test the Nightscout config flow."""
-from aiohttp import ClientConnectionError
+from aiohttp import ClientConnectionError, ClientResponseError
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.nightscout.const import DOMAIN
@@ -8,7 +8,11 @@
from tests.async_mock import patch
from tests.common import MockConfigEntry
-from tests.components.nightscout import GLUCOSE_READINGS, SERVER_STATUS
+from tests.components.nightscout import (
+ GLUCOSE_READINGS,
+ SERVER_STATUS,
+ SERVER_STATUS_STATUS_ONLY,
+)
CONFIG = {CONF_URL: "https://some.url:1234"}
@@ -55,6 +59,28 @@ async def test_user_form_cannot_connect(hass):
assert result2["errors"] == {"base": "cannot_connect"}
+async def test_user_form_api_key_required(hass):
+ """Test we handle an unauthorized error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.nightscout.NightscoutAPI.get_server_status",
+ return_value=SERVER_STATUS_STATUS_ONLY,
+ ), patch(
+ "homeassistant.components.nightscout.NightscoutAPI.get_sgvs",
+ side_effect=ClientResponseError(None, None, status=401),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "https://some.url:1234"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
async def test_user_form_unexpected_exception(hass):
"""Test we handle unexpected exception."""
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py
index 6aaf7df95051d6..7a887ac3468a8d 100644
--- a/tests/components/notion/test_config_flow.py
+++ b/tests/components/notion/test_config_flow.py
@@ -53,7 +53,7 @@ async def test_invalid_credentials(hass, mock_aionotion):
flow.context = {"source": SOURCE_USER}
result = await flow.async_step_user(user_input=conf)
- assert result["errors"] == {"base": "invalid_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
async def test_show_form(hass):
diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py
index 42fdc09e2b1728..4950d467d6afcb 100644
--- a/tests/components/nut/test_sensor.py
+++ b/tests/components/nut/test_sensor.py
@@ -72,7 +72,7 @@ async def test_5e850i(hass):
"device_class": "battery",
"friendly_name": "Ups1 Battery Charge",
"state": "Online",
- "unit_of_measurement": "%",
+ "unit_of_measurement": PERCENTAGE,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
@@ -97,7 +97,7 @@ async def test_5e650i(hass):
"device_class": "battery",
"friendly_name": "Ups1 Battery Charge",
"state": "Online Battery Charging",
- "unit_of_measurement": "%",
+ "unit_of_measurement": PERCENTAGE,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
@@ -125,7 +125,7 @@ async def test_backupsses600m1(hass):
"device_class": "battery",
"friendly_name": "Ups1 Battery Charge",
"state": "Online",
- "unit_of_measurement": "%",
+ "unit_of_measurement": PERCENTAGE,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py
index 9d4acd7dde1990..44b5193d79c5c6 100644
--- a/tests/components/nws/test_init.py
+++ b/tests/components/nws/test_init.py
@@ -17,7 +17,7 @@ async def test_unload_entry(hass, mock_simple_nws):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 2
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1
assert DOMAIN in hass.data
assert len(hass.data[DOMAIN]) == 1
diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py
index 2604c6f39ace90..bd8d81a4b0f88c 100644
--- a/tests/components/nws/test_weather.py
+++ b/tests/components/nws/test_weather.py
@@ -5,7 +5,11 @@
import pytest
from homeassistant.components import nws
-from homeassistant.components.weather import ATTR_CONDITION_SUNNY, ATTR_FORECAST
+from homeassistant.components.weather import (
+ ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST,
+ DOMAIN as WEATHER_DOMAIN,
+)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -35,6 +39,16 @@ async def test_imperial_metric(
hass, units, result_observation, result_forecast, mock_simple_nws
):
"""Test with imperial and metric units."""
+ # enable the hourly entity
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ registry.async_get_or_create(
+ WEATHER_DOMAIN,
+ nws.DOMAIN,
+ "35_-75_hourly",
+ suggested_object_id="abc_hourly",
+ disabled_by=None,
+ )
+
hass.config.units = units
entry = MockConfigEntry(
domain=nws.DOMAIN,
@@ -201,10 +215,6 @@ def increment_time(time):
assert state
assert state.state == STATE_UNAVAILABLE
- state = hass.states.get("weather.abc_hourly")
- assert state
- assert state.state == STATE_UNAVAILABLE
-
# second update happens faster and succeeds
instance.update_observation.side_effect = None
increment_time(timedelta(minutes=1))
@@ -216,10 +226,6 @@ def increment_time(time):
assert state
assert state.state == ATTR_CONDITION_SUNNY
- state = hass.states.get("weather.abc_hourly")
- assert state
- assert state.state == "sunny"
-
# third udate fails, but data is cached
instance.update_observation.side_effect = aiohttp.ClientError
@@ -232,10 +238,6 @@ def increment_time(time):
assert state
assert state.state == ATTR_CONDITION_SUNNY
- state = hass.states.get("weather.abc_hourly")
- assert state
- assert state.state == ATTR_CONDITION_SUNNY
-
# after 20 minutes data caching expires, data is no longer shown
increment_time(timedelta(minutes=10))
await hass.async_block_till_done()
@@ -244,10 +246,6 @@ def increment_time(time):
assert state
assert state.state == STATE_UNAVAILABLE
- state = hass.states.get("weather.abc_hourly")
- assert state
- assert state.state == STATE_UNAVAILABLE
-
async def test_error_forecast(hass, mock_simple_nws):
"""Test error during update forecast."""
@@ -285,6 +283,16 @@ async def test_error_forecast_hourly(hass, mock_simple_nws):
instance = mock_simple_nws.return_value
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
+ # enable the hourly entity
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ registry.async_get_or_create(
+ WEATHER_DOMAIN,
+ nws.DOMAIN,
+ "35_-75_hourly",
+ suggested_object_id="abc_hourly",
+ disabled_by=None,
+ )
+
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
@@ -309,3 +317,30 @@ async def test_error_forecast_hourly(hass, mock_simple_nws):
state = hass.states.get("weather.abc_hourly")
assert state
assert state.state == ATTR_CONDITION_SUNNY
+
+
+async def test_forecast_hourly_disable_enable(hass, mock_simple_nws):
+ """Test error during update forecast hourly."""
+ entry = MockConfigEntry(
+ domain=nws.DOMAIN,
+ data=NWS_CONFIG,
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get_or_create(
+ WEATHER_DOMAIN,
+ nws.DOMAIN,
+ "35_-75_hourly",
+ )
+ assert entry.disabled is True
+
+ # 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
diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py
index 8a36b299d87ef0..27dc47a6df4f14 100644
--- a/tests/components/nzbget/__init__.py
+++ b/tests/components/nzbget/__init__.py
@@ -26,6 +26,8 @@
CONF_VERIFY_SSL: False,
}
+ENTRY_OPTIONS = {CONF_SCAN_INTERVAL: 5}
+
USER_INPUT = {
CONF_HOST: "10.10.10.30",
CONF_NAME: "NZBGet",
@@ -50,12 +52,12 @@
MOCK_STATUS = {
"ArticleCacheMB": 64,
"AverageDownloadRate": 1250000,
- "DownloadPaused": 4,
+ "DownloadPaused": False,
"DownloadRate": 2500000,
"DownloadedSizeMB": 256,
"FreeDiskSpaceMB": 1024,
"PostJobCount": 2,
- "PostPaused": 4,
+ "PostPaused": False,
"RemainingSizeMB": 512,
"UpTimeSec": 600,
}
@@ -69,17 +71,15 @@
async def init_integration(
hass,
*,
- status: dict = MOCK_STATUS,
- history: dict = MOCK_HISTORY,
- version: str = MOCK_VERSION,
+ data: dict = ENTRY_CONFIG,
+ options: dict = ENTRY_OPTIONS,
) -> MockConfigEntry:
"""Set up the NZBGet integration in Home Assistant."""
- entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
+ entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
entry.add_to_hass(hass)
- with _patch_version(version), _patch_status(status), _patch_history(history):
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
return entry
diff --git a/tests/components/nzbget/conftest.py b/tests/components/nzbget/conftest.py
new file mode 100644
index 00000000000000..5855253b1d19a6
--- /dev/null
+++ b/tests/components/nzbget/conftest.py
@@ -0,0 +1,21 @@
+"""Define fixtures available for all tests."""
+from pytest import fixture
+
+from . import MOCK_HISTORY, MOCK_STATUS, MOCK_VERSION
+
+from tests.async_mock import MagicMock, patch
+
+
+@fixture
+def nzbget_api(hass):
+ """Mock NZBGetApi for easier testing."""
+ with patch("homeassistant.components.nzbget.coordinator.NZBGetAPI") as mock_api:
+ instance = mock_api.return_value
+
+ instance.history = MagicMock(return_value=list(MOCK_HISTORY))
+ instance.pausedownload = MagicMock(return_value=True)
+ instance.resumedownload = MagicMock(return_value=True)
+ instance.status = MagicMock(return_value=MOCK_STATUS.copy())
+ instance.version = MagicMock(return_value=MOCK_VERSION)
+
+ yield mock_api
diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py
index 362ba25ff67eb0..a58d1faa766d71 100644
--- a/tests/components/nzbget/test_config_flow.py
+++ b/tests/components/nzbget/test_config_flow.py
@@ -132,7 +132,7 @@ async def test_user_form_single_instance_allowed(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_options_flow(hass):
+async def test_options_flow(hass, nzbget_api):
"""Test updating options."""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -141,16 +141,22 @@ async def test_options_flow(hass):
)
entry.add_to_hass(hass)
+ with patch("homeassistant.components.nzbget.PLATFORMS", []):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
assert entry.options[CONF_SCAN_INTERVAL] == 5
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "init"
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={CONF_SCAN_INTERVAL: 15},
- )
+ with _patch_async_setup(), _patch_async_setup_entry():
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_SCAN_INTERVAL: 15},
+ )
+ await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_SCAN_INTERVAL] == 15
diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py
index 62532c56699856..d24a33d1f5b6f1 100644
--- a/tests/components/nzbget/test_init.py
+++ b/tests/components/nzbget/test_init.py
@@ -38,7 +38,7 @@ async def test_import_from_yaml(hass) -> None:
assert entries[0].data[CONF_PORT] == 6789
-async def test_unload_entry(hass):
+async def test_unload_entry(hass, nzbget_api):
"""Test successful unload of entry."""
entry = await init_integration(hass)
diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py
index 43803384740cbc..c9f1ea71f0cb9a 100644
--- a/tests/components/nzbget/test_sensor.py
+++ b/tests/components/nzbget/test_sensor.py
@@ -14,10 +14,10 @@
from tests.async_mock import patch
-async def test_sensors(hass) -> None:
+async def test_sensors(hass, nzbget_api) -> None:
"""Test the creation and values of the sensors."""
now = dt_util.utcnow().replace(microsecond=0)
- with patch("homeassistant.util.dt.utcnow", return_value=now):
+ with patch("homeassistant.components.nzbget.sensor.utcnow", return_value=now):
entry = await init_integration(hass)
registry = await hass.helpers.entity_registry.async_get_registry()
@@ -32,12 +32,12 @@ async def test_sensors(hass) -> None:
DATA_RATE_MEGABYTES_PER_SECOND,
None,
),
- "download_paused": ("DownloadPaused", "4", None, None),
+ "download_paused": ("DownloadPaused", "False", None, None),
"speed": ("DownloadRate", "2.38", DATA_RATE_MEGABYTES_PER_SECOND, None),
"size": ("DownloadedSizeMB", "256", DATA_MEGABYTES, None),
"disk_free": ("FreeDiskSpaceMB", "1024", DATA_MEGABYTES, None),
"post_processing_jobs": ("PostJobCount", "2", "Jobs", None),
- "post_processing_paused": ("PostPaused", "4", None, None),
+ "post_processing_paused": ("PostPaused", "False", None, None),
"queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None),
"uptime": ("UpTimeSec", uptime.isoformat(), None, DEVICE_CLASS_TIMESTAMP),
}
diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py
new file mode 100644
index 00000000000000..c12fe8ca526e10
--- /dev/null
+++ b/tests/components/nzbget/test_switch.py
@@ -0,0 +1,64 @@
+"""Test the NZBGet switches."""
+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 . import init_integration
+
+
+async def test_download_switch(hass, nzbget_api) -> None:
+ """Test the creation and values of the download switch."""
+ instance = nzbget_api.return_value
+
+ entry = await init_integration(hass)
+ assert entry
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_id = "switch.nzbgettest_download"
+ entity_entry = registry.async_get(entity_id)
+ assert entity_entry
+ assert entity_entry.unique_id == f"{entry.entry_id}_download"
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_ON
+
+ # test download paused
+ instance.status.return_value["DownloadPaused"] = True
+
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == STATE_OFF
+
+
+async def test_download_switch_services(hass, nzbget_api) -> None:
+ """Test download switch services."""
+ instance = nzbget_api.return_value
+
+ entry = await init_integration(hass)
+ entity_id = "switch.nzbgettest_download"
+ assert entry
+
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ )
+ instance.pausedownload.assert_called_once()
+
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ )
+ instance.resumedownload.assert_called_once()
diff --git a/tests/components/omnilogic/__init__.py b/tests/components/omnilogic/__init__.py
new file mode 100644
index 00000000000000..b7b8008abaa5dc
--- /dev/null
+++ b/tests/components/omnilogic/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Omnilogic integration."""
diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py
new file mode 100644
index 00000000000000..ef29ff9f67439c
--- /dev/null
+++ b/tests/components/omnilogic/test_config_flow.py
@@ -0,0 +1,147 @@
+"""Test the Omnilogic config flow."""
+from omnilogic import LoginException, OmniLogicException
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.omnilogic.const import DOMAIN
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+DATA = {"username": "test-username", "password": "test-password"}
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ 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"] == {}
+
+ with patch(
+ "homeassistant.components.omnilogic.config_flow.OmniLogic.connect",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.omnilogic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.omnilogic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Omnilogic"
+ assert result2["data"] == DATA
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_already_configured(hass):
+ """Test config flow when Omnilogic component is already setup."""
+ MockConfigEntry(domain="omnilogic", data=DATA).add_to_hass(hass)
+
+ 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"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_with_invalid_credentials(hass):
+ """Test with invalid credentials."""
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.omnilogic.OmniLogic.connect",
+ side_effect=LoginException,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test if invalid response or no connection returned from Hayward."""
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.omnilogic.OmniLogic.connect",
+ side_effect=OmniLogicException,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_with_unknown_error(hass):
+ """Test with unknown error response from Hayward."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.omnilogic.OmniLogic.connect",
+ side_effect=Exception,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_option_flow(hass):
+ """Test option flow."""
+ entry = MockConfigEntry(domain=DOMAIN, data=DATA)
+ entry.add_to_hass(hass)
+
+ assert not entry.options
+
+ with patch(
+ "homeassistant.components.omnilogic.async_setup_entry", return_value=True
+ ):
+ 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={"polling_interval": 9},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"]["polling_interval"] == 9
diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py
index 0d42564262215f..a1b857a52ceb00 100644
--- a/tests/components/onboarding/test_views.py
+++ b/tests/components/onboarding/test_views.py
@@ -1,5 +1,6 @@
"""Test the onboarding views."""
import asyncio
+import os
import pytest
@@ -29,6 +30,57 @@ def auth_active(hass):
)
+@pytest.fixture(name="rpi")
+async def rpi_fixture(hass, aioclient_mock, mock_supervisor):
+ """Mock core info with rpi."""
+ aioclient_mock.get(
+ "http://127.0.0.1/core/info",
+ json={
+ "result": "ok",
+ "data": {"version_latest": "1.0.0", "machine": "raspberrypi3"},
+ },
+ )
+ assert await async_setup_component(hass, "hassio", {})
+ await hass.async_block_till_done()
+
+
+@pytest.fixture(name="no_rpi")
+async def no_rpi_fixture(hass, aioclient_mock, mock_supervisor):
+ """Mock core info with rpi."""
+ aioclient_mock.get(
+ "http://127.0.0.1/core/info",
+ json={
+ "result": "ok",
+ "data": {"version_latest": "1.0.0", "machine": "odroid-n2"},
+ },
+ )
+ assert await async_setup_component(hass, "hassio", {})
+ await hass.async_block_till_done()
+
+
+@pytest.fixture(name="mock_supervisor")
+async def mock_supervisor_fixture(hass, aioclient_mock):
+ """Mock supervisor."""
+ aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
+ aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
+ with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch(
+ "homeassistant.components.hassio.HassIO.is_connected",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_info",
+ return_value={},
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_host_info",
+ return_value={},
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_ingress_panels",
+ return_value={"panels": {}},
+ ), patch.dict(
+ os.environ, {"HASSIO_TOKEN": "123456"}
+ ):
+ yield
+
+
async def test_onboarding_progress(hass, hass_storage, aiohttp_client):
"""Test fetching progress."""
mock_storage(hass_storage, {"done": ["hello"]})
@@ -277,3 +329,51 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client):
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 1
+
+
+async def test_onboarding_core_sets_up_rpi_power(
+ hass, hass_storage, hass_client, aioclient_mock, rpi
+):
+ """Test that the core step sets up rpi_power on RPi."""
+ mock_storage(hass_storage, {"done": [const.STEP_USER]})
+ await async_setup_component(hass, "persistent_notification", {})
+
+ assert await async_setup_component(hass, "onboarding", {})
+
+ 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")
+
+ assert resp.status == 200
+
+ 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
+):
+ """Test that the core step do not set up rpi_power on non RPi."""
+ mock_storage(hass_storage, {"done": [const.STEP_USER]})
+ await async_setup_component(hass, "persistent_notification", {})
+
+ assert await async_setup_component(hass, "onboarding", {})
+
+ 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")
+
+ assert resp.status == 200
+
+ await hass.async_block_till_done()
+
+ rpi_power_state = hass.states.get("binary_sensor.rpi_power_status")
+ assert not rpi_power_state
diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py
index 864968f848d159..e54b0c7a4ecc72 100644
--- a/tests/components/onvif/test_config_flow.py
+++ b/tests/components/onvif/test_config_flow.py
@@ -531,7 +531,7 @@ async def test_flow_import_onvif_auth_error(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
- assert result["errors"]["base"] == "connection_failed"
+ assert result["errors"]["base"] == "cannot_connect"
async def test_option_flow(hass):
diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py
index 672e7358803616..4b3297563ed30d 100644
--- a/tests/components/openweathermap/test_config_flow.py
+++ b/tests/components/openweathermap/test_config_flow.py
@@ -1,5 +1,4 @@
"""Define tests for the OpenWeatherMap config flow."""
-from asynctest import MagicMock, patch
from pyowm.exceptions.api_call_error import APICallError
from pyowm.exceptions.api_response_error import UnauthorizedError
@@ -19,6 +18,7 @@
CONF_NAME,
)
+from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
CONFIG = {
@@ -166,7 +166,7 @@ async def test_form_invalid_api_key(hass):
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
- assert result["errors"] == {"base": "auth"}
+ assert result["errors"] == {"base": "invalid_api_key"}
async def test_form_api_call_error(hass):
@@ -182,7 +182,7 @@ async def test_form_api_call_error(hass):
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
- assert result["errors"] == {"base": "connection"}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_form_api_offline(hass):
@@ -197,7 +197,7 @@ async def test_form_api_offline(hass):
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
- assert result["errors"] == {"base": "auth"}
+ assert result["errors"] == {"base": "invalid_api_key"}
def _create_mocked_owm(is_api_online: bool):
diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py
index 396870c2975457..2290e3e17a47a1 100644
--- a/tests/components/owntracks/test_config_flow.py
+++ b/tests/components/owntracks/test_config_flow.py
@@ -111,12 +111,12 @@ async def test_abort_if_already_setup(hass):
# Should fail, already setup (import)
result = await flow.async_step_import({})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
# Should fail, already setup (flow)
result = await flow.async_step_user({})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
async def test_user_not_supports_encryption(hass, not_supports_encryption):
diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py
index 26295e79b07950..3414e6c483279a 100644
--- a/tests/components/ozw/test_climate.py
+++ b/tests/components/ozw/test_climate.py
@@ -281,3 +281,47 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
)
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
index 3446bbfc7defcd..1af6787a9528c3 100644
--- a/tests/components/ozw/test_config_flow.py
+++ b/tests/components/ozw/test_config_flow.py
@@ -51,4 +51,4 @@ async def test_one_instance_allowed(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "abort"
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/ozw/test_lock.py b/tests/components/ozw/test_lock.py
index c85bd198bdda3f..f32d073c562182 100644
--- a/tests/components/ozw/test_lock.py
+++ b/tests/components/ozw/test_lock.py
@@ -80,7 +80,4 @@ async def test_lock(hass, lock_data, sent_messages, lock_msg, caplog):
blocking=True,
)
assert len(sent_messages) == 5
- assert (
- "Invalid code provided: (123) user code must be at least 4 digits"
- in caplog.text
- )
+ assert "User code must be at least 4 digits" in caplog.text
diff --git a/tests/components/ozw/test_services.py b/tests/components/ozw/test_services.py
index 1460e69b9c9d1f..7c71b234242fc0 100644
--- a/tests/components/ozw/test_services.py
+++ b/tests/components/ozw/test_services.py
@@ -1,8 +1,12 @@
"""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, light_msg, caplog):
+async def test_services(hass, light_data, sent_messages):
"""Test services on lock."""
await setup_ozw(hass, fixture=light_data)
@@ -43,34 +47,48 @@ async def test_services(hass, light_data, sent_messages, light_msg, caplog):
assert msg["payload"] == {"Value": 55, "ValueIDKey": 844425594667027}
# Test set_config_parameter invalid list int
- await hass.services.async_call(
- "ozw",
- "set_config_parameter",
- {"node_id": 39, "parameter": 1, "value": 12},
- blocking=True,
- )
+ 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
- assert "Invalid value 12 for parameter 1" in caplog.text
- # Test set_config_parameter invalid list string
- await hass.services.async_call(
- "ozw",
- "set_config_parameter",
- {"node_id": 39, "parameter": 1, "value": "Blah"},
- blocking=True,
- )
+ # 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
- assert "Invalid value Blah for parameter 1" in caplog.text
# Test set_config_parameter int out of range
- await hass.services.async_call(
- "ozw",
- "set_config_parameter",
- {"node_id": 39, "parameter": 3, "value": 2147483657},
- blocking=True,
- )
+ 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
- assert "Value 2147483657 out of range for parameter 3" in caplog.text
# Test set_config_parameter short
await hass.services.async_call(
diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py
index 353615c18124d6..d4194b0a537935 100644
--- a/tests/components/ozw/test_websocket_api.py
+++ b/tests/components/ozw/test_websocket_api.py
@@ -1,5 +1,15 @@
"""Test OpenZWave Websocket API."""
+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,
@@ -17,9 +27,15 @@
ID,
NODE_ID,
OZW_INSTANCE,
+ PARAMETER,
TYPE,
+ VALUE,
+)
+from homeassistant.components.websocket_api.const import (
+ ERR_INVALID_FORMAT,
+ ERR_NOT_FOUND,
+ ERR_NOT_SUPPORTED,
)
-from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
from .common import MQTTMessage, setup_ozw
@@ -119,6 +135,188 @@ async def test_websocket_api(hass, generic_data, hass_ws_client):
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["Value"]
+ for option in config_param[ATTR_OPTIONS]
+ if option["Label"] != current_val
+ )
+ new_label = next(
+ option["Label"]
+ for option in config_param[ATTR_OPTIONS]
+ if option["Label"] != current_val and option["Value"] != 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):
+ """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):
"""Test the ozw refresh node api."""
diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py
index 5359811c52c9c8..39d6913e3a8989 100644
--- a/tests/components/panasonic_viera/test_config_flow.py
+++ b/tests/components/panasonic_viera/test_config_flow.py
@@ -11,9 +11,6 @@
DEFAULT_PORT,
DOMAIN,
ERROR_INVALID_PIN_CODE,
- ERROR_NOT_CONNECTED,
- REASON_NOT_CONNECTED,
- REASON_UNKNOWN,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
@@ -116,7 +113,7 @@ async def test_flow_not_connected_error(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
- assert result["errors"] == {"base": ERROR_NOT_CONNECTED}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_flow_unknown_abort(hass):
@@ -139,7 +136,7 @@ async def test_flow_unknown_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_UNKNOWN
+ assert result["reason"] == "unknown"
async def test_flow_encrypted_valid_pin_code(hass):
@@ -255,7 +252,7 @@ async def test_flow_encrypted_not_connected_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_NOT_CONNECTED
+ assert result["reason"] == "cannot_connect"
async def test_flow_encrypted_unknown_abort(hass):
@@ -288,7 +285,7 @@ async def test_flow_encrypted_unknown_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_UNKNOWN
+ assert result["reason"] == "unknown"
async def test_flow_non_encrypted_already_configured_abort(hass):
@@ -475,7 +472,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_NOT_CONNECTED
+ assert result["reason"] == "cannot_connect"
async def test_imported_flow_encrypted_unknown_abort(hass):
@@ -507,7 +504,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_UNKNOWN
+ assert result["reason"] == "unknown"
async def test_imported_flow_not_connected_error(hass):
@@ -530,7 +527,7 @@ async def test_imported_flow_not_connected_error(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
- assert result["errors"] == {"base": ERROR_NOT_CONNECTED}
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_imported_flow_unknown_abort(hass):
@@ -552,7 +549,7 @@ async def test_imported_flow_unknown_abort(hass):
)
assert result["type"] == "abort"
- assert result["reason"] == REASON_UNKNOWN
+ assert result["reason"] == "unknown"
async def test_imported_flow_non_encrypted_already_configured_abort(hass):
diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py
index 866702488dc9af..f30350141c4e43 100644
--- a/tests/components/plant/test_init.py
+++ b/tests/components/plant/test_init.py
@@ -8,6 +8,7 @@
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONDUCTIVITY,
+ LIGHT_LUX,
STATE_OK,
STATE_PROBLEM,
STATE_UNAVAILABLE,
@@ -187,17 +188,17 @@ async def test_brightness_history(hass):
assert await async_setup_component(
hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}}
)
- hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"})
+ hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
assert STATE_PROBLEM == state.state
- hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: "lux"})
+ hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
assert STATE_OK == state.state
- hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"})
+ hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
assert STATE_OK == state.state
diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py
new file mode 100644
index 00000000000000..4e59b55157410f
--- /dev/null
+++ b/tests/components/plex/conftest.py
@@ -0,0 +1,59 @@
+"""Fixtures for Plex tests."""
+import pytest
+
+from homeassistant.components.plex.const import DOMAIN
+
+from .const import DEFAULT_DATA, DEFAULT_OPTIONS
+from .mock_classes import MockPlexAccount, MockPlexServer
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="entry")
+def mock_config_entry():
+ """Return the default mocked config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ data=DEFAULT_DATA,
+ options=DEFAULT_OPTIONS,
+ unique_id=DEFAULT_DATA["server_id"],
+ )
+
+
+@pytest.fixture
+def mock_plex_account():
+ """Mock the PlexAccount class and return the used instance."""
+ plex_account = MockPlexAccount()
+ with patch("plexapi.myplex.MyPlexAccount", return_value=plex_account):
+ yield plex_account
+
+
+@pytest.fixture
+def mock_websocket():
+ """Mock the PlexWebsocket class."""
+ with patch("homeassistant.components.plex.PlexWebsocket", autospec=True) as ws:
+ yield ws
+
+
+@pytest.fixture
+def setup_plex_server(hass, entry, mock_plex_account, mock_websocket):
+ """Set up and return a mocked Plex server instance."""
+
+ async def _wrapper(**kwargs):
+ """Wrap the fixture to allow passing arguments to the MockPlexServer instance."""
+ config_entry = kwargs.get("config_entry", entry)
+ plex_server = MockPlexServer(**kwargs)
+ with patch("plexapi.server.PlexServer", return_value=plex_server):
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ return plex_server
+
+ return _wrapper
+
+
+@pytest.fixture
+async def mock_plex_server(entry, setup_plex_server):
+ """Init from a config entry and return a mocked PlexServer instance."""
+ return await setup_plex_server(config_entry=entry)
diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py
index 8055ab0d5b19fd..a20d70fbb7e8e4 100644
--- a/tests/components/plex/helpers.py
+++ b/tests/components/plex/helpers.py
@@ -1,7 +1,8 @@
"""Helper methods for Plex tests."""
+from plexwebsocket import SIGNAL_DATA
def trigger_plex_update(mock_websocket):
"""Call the websocket callback method."""
callback = mock_websocket.call_args[0][1]
- callback()
+ callback(SIGNAL_DATA, None, None)
diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py
index 9cf6d7a7332607..d96fdd4a00bad0 100644
--- a/tests/components/plex/test_browse_media.py
+++ b/tests/components/plex/test_browse_media.py
@@ -3,39 +3,16 @@
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
)
-from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, DOMAIN
+from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER
from homeassistant.components.plex.media_browser import SPECIAL_METHODS
from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT
-from .const import DEFAULT_DATA, DEFAULT_OPTIONS
+from .const import DEFAULT_DATA
from .helpers import trigger_plex_update
-from .mock_classes import MockPlexAccount, MockPlexServer
-from tests.async_mock import patch
-from tests.common import MockConfigEntry
-
-async def test_browse_media(hass, hass_ws_client):
+async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket):
"""Test getting Plex clients from plex.tv."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
- mock_plex_account = MockPlexAccount()
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
websocket_client = await hass_ws_client(hass)
trigger_plex_update(mock_websocket)
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index 1bd2ce82863201..476c342f1769af 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -24,6 +24,7 @@
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
SOURCE_INTEGRATION_DISCOVERY,
+ SOURCE_REAUTH,
)
from homeassistant.const import (
CONF_HOST,
@@ -34,7 +35,7 @@
CONF_VERIFY_SSL,
)
-from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
+from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
from .helpers import trigger_plex_update
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer, MockResource
@@ -77,8 +78,6 @@ async def test_bad_credentials(hass):
async def test_bad_hostname(hass):
"""Test when an invalid address is provided."""
- mock_plex_account = MockPlexAccount()
-
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@@ -91,7 +90,7 @@ async def test_bad_hostname(hass):
assert result["step_id"] == "user"
with patch(
- "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
+ "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
), patch.object(
MockResource, "connect", side_effect=requests.exceptions.ConnectionError
), patch(
@@ -363,24 +362,8 @@ async def test_all_available_servers_configured(hass):
assert result["reason"] == "all_configured"
-async def test_option_flow(hass):
+async def test_option_flow(hass, entry, mock_plex_server):
"""Test config options flow selection."""
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
@@ -411,24 +394,8 @@ async def test_option_flow(hass):
}
-async def test_missing_option_flow(hass):
+async def test_missing_option_flow(hass, entry, mock_plex_server):
"""Test config options flow selection when no options stored."""
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=None,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
@@ -459,29 +426,15 @@ async def test_missing_option_flow(hass):
}
-async def test_option_flow_new_users_available(hass, caplog):
+async def test_option_flow_new_users_available(
+ hass, caplog, entry, mock_websocket, setup_plex_server
+):
"""Test config options multiselect defaults when new Plex users are seen."""
-
OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}}
+ entry.options = OPTIONS_OWNER_ONLY
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=OPTIONS_OWNER_ONLY,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ mock_plex_server = await setup_plex_server(config_entry=entry)
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
@@ -734,29 +687,12 @@ async def test_manual_config_with_token(hass):
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
-async def test_setup_with_limited_credentials(hass):
+async def test_setup_with_limited_credentials(hass, entry, setup_plex_server):
"""Test setup with a user with limited permissions."""
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch(
- "plexapi.server.PlexServer", return_value=mock_plex_server
- ), patch.object(
- mock_plex_server, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized
- ) as mock_accounts, patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ with patch.object(
+ MockPlexServer, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized
+ ) as mock_accounts:
+ mock_plex_server = await setup_plex_server()
assert mock_accounts.called
@@ -788,3 +724,53 @@ async def test_integration_discovery(hass):
== mock_gdm.entries[0]["data"]["Resource-Identifier"]
)
assert flow["step_id"] == "user"
+
+
+async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
+ """Test setup and reauthorization of a Plex token."""
+ await async_process_ha_core_config(
+ hass,
+ {"internal_url": "http://example.local:8123"},
+ )
+
+ assert entry.state == ENTRY_STATE_LOADED
+
+ with patch.object(
+ mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized
+ ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized):
+ trigger_plex_update(mock_websocket)
+ await hass.async_block_till_done()
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state != ENTRY_STATE_LOADED
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+ assert flows[0]["context"]["source"] == SOURCE_REAUTH
+
+ flow_id = flows[0]["flow_id"]
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
+ "plexapi.server.PlexServer", return_value=mock_plex_server
+ ), patch("plexauth.PlexAuth.initiate_auth"), patch(
+ "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"
+ ):
+ result = await hass.config_entries.flow.async_configure(flow_id, user_input={})
+ assert result["type"] == "external"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "external_done"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == "abort"
+ assert result["reason"] == "reauth_successful"
+ assert result["flow_id"] == flow_id
+
+ assert len(hass.config_entries.flow.async_progress()) == 0
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+ assert entry.state == ENTRY_STATE_LOADED
+ assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName
+ assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
+ assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
+ assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN"
diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py
index 666a819e8ca07e..3c4f9031fad1a8 100644
--- a/tests/components/plex/test_init.py
+++ b/tests/components/plex/test_init.py
@@ -24,27 +24,8 @@
from tests.common import MockConfigEntry, async_fire_time_changed
-async def test_set_config_entry_unique_id(hass):
+async def test_set_config_entry_unique_id(hass, entry, mock_plex_server):
"""Test updating missing unique_id from config entry."""
-
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=None,
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- assert mock_listen.called
-
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
@@ -54,16 +35,8 @@ async def test_set_config_entry_unique_id(hass):
)
-async def test_setup_config_entry_with_error(hass):
+async def test_setup_config_entry_with_error(hass, entry):
"""Test setup component from config entry with errors."""
-
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
with patch(
"homeassistant.components.plex.PlexServer.connect",
side_effect=requests.exceptions.ConnectionError,
@@ -87,91 +60,38 @@ async def test_setup_config_entry_with_error(hass):
assert entry.state == ENTRY_STATE_SETUP_ERROR
-async def test_setup_with_insecure_config_entry(hass):
+async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server):
"""Test setup component with config."""
-
- mock_plex_server = MockPlexServer()
-
INSECURE_DATA = copy.deepcopy(DEFAULT_DATA)
INSECURE_DATA[const.PLEX_SERVER_CONFIG][CONF_VERIFY_SSL] = False
+ entry.data = INSECURE_DATA
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=INSECURE_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- assert mock_listen.called
+ await setup_plex_server(config_entry=entry)
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
-async def test_unload_config_entry(hass):
+async def test_unload_config_entry(hass, entry, mock_plex_server):
"""Test unloading a config entry."""
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
- entry.add_to_hass(hass)
-
config_entries = hass.config_entries.async_entries(const.DOMAIN)
assert len(config_entries) == 1
assert entry is config_entries[0]
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen:
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
- assert mock_listen.called
-
assert entry.state == ENTRY_STATE_LOADED
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id]
-
assert loaded_server.plex_server == mock_plex_server
- with patch("homeassistant.components.plex.PlexWebsocket.close") as mock_close:
- await hass.config_entries.async_unload(entry.entry_id)
- assert mock_close.called
-
+ websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id]
+ await hass.config_entries.async_unload(entry.entry_id)
+ assert websocket.close.called
assert entry.state == ENTRY_STATE_NOT_LOADED
-async def test_setup_with_photo_session(hass):
+async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server):
"""Test setup component with config."""
-
- mock_plex_server = MockPlexServer(session_type="photo")
-
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ mock_plex_server = await setup_plex_server(config_entry=entry, session_type="photo")
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
@@ -186,7 +106,7 @@ async def test_setup_with_photo_session(hass):
assert sensor.state == str(len(mock_plex_server.accounts))
-async def test_setup_when_certificate_changed(hass):
+async def test_setup_when_certificate_changed(hass, entry):
"""Test setup component when the Plex certificate has changed."""
old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct"
@@ -210,8 +130,6 @@ def __init__(self):
unique_id=DEFAULT_DATA["server_id"],
)
- new_entry = MockConfigEntry(domain=const.DOMAIN, data=DEFAULT_DATA)
-
# Test with account failure
with patch(
"plexapi.server.PlexServer", side_effect=WrongCertHostnameException
@@ -247,49 +165,23 @@ def __init__(self):
assert (
old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL]
- == new_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL]
+ == entry.data[const.PLEX_SERVER_CONFIG][CONF_URL]
)
-async def test_tokenless_server(hass):
+async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server):
"""Test setup with a server with token auth disabled."""
- mock_plex_server = MockPlexServer()
-
TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA)
TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None)
+ entry.data = TOKENLESS_DATA
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=TOKENLESS_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
+ await setup_plex_server(config_entry=entry)
assert entry.state == ENTRY_STATE_LOADED
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
-
-async def test_bad_token_with_tokenless_server(hass):
+async def test_bad_token_with_tokenless_server(hass, entry):
"""Test setup with a bad token and a server with token auth disabled."""
- mock_plex_server = MockPlexServer()
-
- entry = MockConfigEntry(
- domain=const.DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
+ with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True
@@ -300,5 +192,6 @@ async def test_bad_token_with_tokenless_server(hass):
assert entry.state == ENTRY_STATE_LOADED
+ # Ensure updates that rely on account return nothing
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py
index d3e2de91cf951f..fdacd6051ae35c 100644
--- a/tests/components/plex/test_media_players.py
+++ b/tests/components/plex/test_media_players.py
@@ -3,32 +3,12 @@
from homeassistant.components.plex.const import DOMAIN, SERVERS
-from .const import DEFAULT_DATA, DEFAULT_OPTIONS
-from .mock_classes import MockPlexAccount, MockPlexServer
-
from tests.async_mock import patch
-from tests.common import MockConfigEntry
-async def test_plex_tv_clients(hass):
+async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server):
"""Test getting Plex clients from plex.tv."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
- mock_plex_account = MockPlexAccount()
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
- ), patch("homeassistant.components.plex.PlexWebsocket.listen"):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
+ mock_plex_server = await setup_plex_server()
server_id = mock_plex_server.machineIdentifier
plex_server = hass.data[DOMAIN][SERVERS][server_id]
@@ -46,12 +26,7 @@ async def test_plex_tv_clients(hass):
# Ensure one more client is discovered
await hass.config_entries.async_unload(entry.entry_id)
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
- ), patch("homeassistant.components.plex.PlexWebsocket.listen"):
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
+ mock_plex_server = await setup_plex_server(config_entry=entry)
plex_server = hass.data[DOMAIN][SERVERS][server_id]
await plex_server._async_update_platforms()
@@ -63,15 +38,10 @@ async def test_plex_tv_clients(hass):
# Ensure only plex.tv resource client is found
await hass.config_entries.async_unload(entry.entry_id)
+ mock_plex_server = await setup_plex_server(config_entry=entry)
mock_plex_server.clear_clients()
mock_plex_server.clear_sessions()
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
- ), patch("homeassistant.components.plex.PlexWebsocket.listen"):
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
plex_server = hass.data[DOMAIN][SERVERS][server_id]
await plex_server._async_update_platforms()
diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py
index b031aff25cdbea..bd694419421947 100644
--- a/tests/components/plex/test_playback.py
+++ b/tests/components/plex/test_playback.py
@@ -10,32 +10,11 @@
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
-from .const import DEFAULT_DATA, DEFAULT_OPTIONS
-from .mock_classes import MockPlexAccount, MockPlexServer
-
from tests.async_mock import patch
-from tests.common import MockConfigEntry
-async def test_sonos_playback(hass):
+async def test_sonos_playback(hass, mock_plex_server):
"""Test playing media on a Sonos speaker."""
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket.listen"):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py
index b3623681f8a8be..b650821b3f23c6 100644
--- a/tests/components/plex/test_server.py
+++ b/tests/components/plex/test_server.py
@@ -40,33 +40,16 @@
)
from tests.async_mock import patch
-from tests.common import MockConfigEntry
-async def test_new_users_available(hass):
+async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server):
"""Test setting up when new users available on Plex server."""
-
MONITORED_USERS = {"Owner": {"enabled": True}}
OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS
+ entry.options = OPTIONS_WITH_USERS
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=OPTIONS_WITH_USERS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ mock_plex_server = await setup_plex_server(config_entry=entry)
server_id = mock_plex_server.machineIdentifier
@@ -83,31 +66,17 @@ async def test_new_users_available(hass):
assert sensor.state == str(len(mock_plex_server.accounts))
-async def test_new_ignored_users_available(hass, caplog):
+async def test_new_ignored_users_available(
+ hass, caplog, entry, mock_websocket, setup_plex_server
+):
"""Test setting up when new users available on Plex server but are ignored."""
-
MONITORED_USERS = {"Owner": {"enabled": True}}
OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True
+ entry.options = OPTIONS_WITH_USERS
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=OPTIONS_WITH_USERS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+ mock_plex_server = await setup_plex_server(config_entry=entry)
server_id = mock_plex_server.machineIdentifier
@@ -134,26 +103,10 @@ async def test_new_ignored_users_available(hass, caplog):
assert sensor.state == str(len(mock_plex_server.accounts))
-async def test_network_error_during_refresh(hass, caplog):
+async def test_network_error_during_refresh(
+ hass, caplog, mock_plex_server, mock_websocket
+):
"""Test network failures during refreshes."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer()
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
@@ -172,26 +125,8 @@ async def test_network_error_during_refresh(hass, caplog):
)
-async def test_mark_sessions_idle(hass):
+async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
"""Test marking media_players as idle when sessions end."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer()
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
@@ -211,26 +146,17 @@ async def test_mark_sessions_idle(hass):
assert sensor.state == "0"
-async def test_ignore_plex_web_client(hass):
+async def test_ignore_plex_web_client(hass, entry, mock_websocket):
"""Test option to ignore Plex Web clients."""
-
OPTIONS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
+ entry.options = OPTIONS
mock_plex_server = MockPlexServer(config_entry=entry)
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
+ ):
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -246,27 +172,8 @@ async def test_ignore_plex_web_client(hass):
assert len(media_players) == int(sensor.state) - 1
-async def test_media_lookups(hass):
+async def test_media_lookups(hass, mock_plex_server, mock_websocket):
"""Test media lookups to Plex server."""
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch(
- "homeassistant.components.plex.PlexWebsocket", autospec=True
- ) as mock_websocket:
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py
index 078ba3b97e961e..a3f4d4c833aeef 100644
--- a/tests/components/plex/test_services.py
+++ b/tests/components/plex/test_services.py
@@ -15,31 +15,15 @@
CONF_VERIFY_SSL,
)
-from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
-from .mock_classes import MockPlexAccount, MockPlexLibrarySection, MockPlexServer
+from .const import MOCK_SERVERS, MOCK_TOKEN
+from .mock_classes import MockPlexLibrarySection
from tests.async_mock import patch
from tests.common import MockConfigEntry
-async def test_refresh_library(hass):
+async def test_refresh_library(hass, mock_plex_server, setup_plex_server):
"""Test refresh_library service call."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
# Test with non-existent server
with patch.object(MockPlexLibrarySection, "update") as mock_update:
assert await hass.services.async_call(
@@ -84,13 +68,7 @@ async def test_refresh_library(hass):
},
)
- mock_plex_server_2 = MockPlexServer(config_entry=entry_2)
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server_2), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
- entry_2.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry_2.entry_id)
- await hass.async_block_till_done()
+ await setup_plex_server(config_entry=entry_2)
# Test multiple servers available but none specified
with patch.object(MockPlexLibrarySection, "update") as mock_update:
@@ -103,24 +81,8 @@ async def test_refresh_library(hass):
assert not mock_update.called
-async def test_scan_clients(hass):
+async def test_scan_clients(hass, mock_plex_server):
"""Test scan_for_clients service call."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data=DEFAULT_DATA,
- options=DEFAULT_OPTIONS,
- unique_id=DEFAULT_DATA["server_id"],
- )
-
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
- ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
assert await hass.services.async_call(
DOMAIN,
SERVICE_SCAN_CLIENTS,
diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py
index 8564b2c0d8cba7..11e077c8a2456e 100644
--- a/tests/components/plugwise/conftest.py
+++ b/tests/components/plugwise/conftest.py
@@ -73,6 +73,7 @@ def mock_smile_adam():
smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730"
smile_mock.return_value.smile_version = "3.0.15"
smile_mock.return_value.smile_type = "thermostat"
+ smile_mock.return_value.smile_hostname = "smile98765"
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
@@ -112,6 +113,7 @@ def mock_smile_anna():
smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927"
smile_mock.return_value.smile_version = "4.0.15"
smile_mock.return_value.smile_type = "thermostat"
+ smile_mock.return_value.smile_hostname = "smile98765"
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
@@ -151,6 +153,7 @@ def mock_smile_p1():
smile_mock.return_value.heater_id = None
smile_mock.return_value.smile_version = "3.3.9"
smile_mock.return_value.smile_type = "power"
+ smile_mock.return_value.smile_hostname = "smile98765"
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py
index 219ba8aee7f1ce..9b67f06e469001 100644
--- a/tests/components/plugwise/test_config_flow.py
+++ b/tests/components/plugwise/test_config_flow.py
@@ -3,7 +3,11 @@
import pytest
from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.components.plugwise.const import DEFAULT_SCAN_INTERVAL, DOMAIN
+from homeassistant.components.plugwise.const import (
+ DEFAULT_PORT,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+)
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
@@ -13,8 +17,11 @@
TEST_HOST = "1.1.1.1"
TEST_HOSTNAME = "smileabcdef"
TEST_PASSWORD = "test_password"
+TEST_PORT = 81
+
TEST_DISCOVERY = {
"host": TEST_HOST,
+ "port": DEFAULT_PORT,
"hostname": f"{TEST_HOSTNAME}.local.",
"server": f"{TEST_HOSTNAME}.local.",
"properties": {
@@ -68,6 +75,7 @@ async def test_form(hass):
assert result2["data"] == {
"host": TEST_HOST,
"password": TEST_PASSWORD,
+ "port": DEFAULT_PORT,
}
assert len(mock_setup.mock_calls) == 1
@@ -106,11 +114,40 @@ async def test_zeroconf_form(hass):
assert result2["data"] == {
"host": TEST_HOST,
"password": TEST_PASSWORD,
+ "port": DEFAULT_PORT,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
+ result3 = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_ZEROCONF},
+ data=TEST_DISCOVERY,
+ )
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["errors"] == {}
+
+ with patch(
+ "homeassistant.components.plugwise.config_flow.Smile.connect",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.plugwise.async_setup",
+ return_value=True,
+ ) as mock_setup, patch(
+ "homeassistant.components.plugwise.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result4 = await hass.config_entries.flow.async_configure(
+ result3["flow_id"],
+ {"password": TEST_PASSWORD},
+ )
+
+ await hass.async_block_till_done()
+
+ assert result4["type"] == "abort"
+ assert result4["reason"] == "already_configured"
+
async def test_form_invalid_auth(hass, mock_smile):
"""Test we handle invalid auth."""
@@ -148,6 +185,24 @@ async def test_form_cannot_connect(hass, mock_smile):
assert result2["errors"] == {"base": "cannot_connect"}
+async def test_form_cannot_connect_port(hass, mock_smile):
+ """Test we handle cannot connect to port error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_smile.connect.side_effect = Smile.ConnectionFailedError
+ mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": TEST_HOST, "password": TEST_PASSWORD, "port": TEST_PORT},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
async def test_form_other_problem(hass, mock_smile):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
diff --git a/tests/components/profiler/__init__.py b/tests/components/profiler/__init__.py
new file mode 100644
index 00000000000000..d3042599223acd
--- /dev/null
+++ b/tests/components/profiler/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Profiler integration."""
diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py
new file mode 100644
index 00000000000000..e6cb62421afd26
--- /dev/null
+++ b/tests/components/profiler/test_config_flow.py
@@ -0,0 +1,45 @@
+"""Test the Profiler config flow."""
+from homeassistant import config_entries, setup
+from homeassistant.components.profiler.const import DOMAIN
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+async def test_form_user(hass):
+ """Test we can setup by the user."""
+ 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"] is None
+
+ with patch(
+ "homeassistant.components.profiler.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.profiler.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Profiler"
+ assert result2["data"] == {}
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_user_only_once(hass):
+ """Test we can setup by the user only once."""
+ MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
+ 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"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py
new file mode 100644
index 00000000000000..2373e64a59369b
--- /dev/null
+++ b/tests/components/profiler/test_init.py
@@ -0,0 +1,41 @@
+"""Test the Profiler config flow."""
+import os
+
+from homeassistant import setup
+from homeassistant.components.profiler import CONF_SECONDS, SERVICE_START
+from homeassistant.components.profiler.const import DOMAIN
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+async def test_basic_usage(hass, tmpdir):
+ """Test we can setup and the service is registered."""
+ test_dir = tmpdir.mkdir("profiles")
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.services.has_service(DOMAIN, SERVICE_START)
+
+ last_filename = None
+
+ def _mock_path(filename):
+ nonlocal last_filename
+ last_filename = f"{test_dir}/{filename}"
+ return last_filename
+
+ with patch("homeassistant.components.profiler.cProfile.Profile"), patch.object(
+ hass.config, "path", _mock_path
+ ):
+ await hass.services.async_call(DOMAIN, SERVICE_START, {CONF_SECONDS: 0.000001})
+ await hass.async_block_till_done()
+
+ assert os.path.exists(last_filename)
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py
index e04cbf9e6326b0..f187429b15195e 100644
--- a/tests/components/prometheus/test_init.py
+++ b/tests/components/prometheus/test_init.py
@@ -9,6 +9,7 @@
import homeassistant.components.prometheus as prometheus
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ CONTENT_TYPE_TEXT_PLAIN,
DEGREE,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
@@ -97,7 +98,7 @@ async def test_view(hass, hass_client):
resp = await client.get(prometheus.API_ENDPOINT)
assert resp.status == 200
- assert resp.headers["content-type"] == "text/plain"
+ assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN
body = await resp.text()
body = body.split("\n")
diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py
index b0fb55e152d09b..12bfc686d179ee 100644
--- a/tests/components/pushbullet/test_notify.py
+++ b/tests/components/pushbullet/test_notify.py
@@ -1,301 +1,273 @@
"""The tests for the pushbullet notification platform."""
import json
-import unittest
from pushbullet import PushBullet
-import requests_mock
+import pytest
import homeassistant.components.notify as notify
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import assert_setup_component, get_test_home_assistant, load_fixture
+from tests.common import assert_setup_component, load_fixture
-class TestPushBullet(unittest.TestCase):
- """Tests the Pushbullet Component."""
-
- def setUp(self):
- """Initialize values for this test case class."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that we started."""
- self.hass.stop()
-
- @patch.object(
+@pytest.fixture
+def mock_pushbullet():
+ """Mock pushbullet."""
+ with patch.object(
PushBullet,
"_get_data",
return_value=json.loads(load_fixture("pushbullet_devices.json")),
- )
- def test_pushbullet_config(self, mock__get_data):
- """Test setup."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
- }
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
+ ):
+ yield
- def test_pushbullet_config_bad(self):
- """Test set up the platform with bad/missing configuration."""
- config = {notify.DOMAIN: {"platform": "pushbullet"}}
- with assert_setup_component(0) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert not handle_config[notify.DOMAIN]
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
- )
- def test_pushbullet_push_default(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
+async def test_pushbullet_config(hass, mock_pushbullet):
+ """Test setup."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {"title": "Test Title", "message": "Test Message"}
- self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
- assert mock.called
- assert mock.call_count == 1
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
- expected_body = {"body": "Test Message", "title": "Test Title", "type": "note"}
- assert mock.last_request.json() == expected_body
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
- )
- def test_pushbullet_push_device(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
- }
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {
- "title": "Test Title",
- "message": "Test Message",
- "target": ["device/DESKTOP"],
- }
- self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
- assert mock.called
- assert mock.call_count == 1
+async def test_pushbullet_config_bad(hass):
+ """Test set up the platform with bad/missing configuration."""
+ config = {notify.DOMAIN: {"platform": "pushbullet"}}
+ with assert_setup_component(0) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert not handle_config[notify.DOMAIN]
- expected_body = {
- "body": "Test Message",
- "device_iden": "identity1",
- "title": "Test Title",
- "type": "note",
- }
- assert mock.last_request.json() == expected_body
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
- )
- def test_pushbullet_push_devices(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
- }
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {
- "title": "Test Title",
- "message": "Test Message",
- "target": ["device/DESKTOP", "device/My iPhone"],
+async def test_pushbullet_push_default(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
- assert mock.called
- assert mock.call_count == 2
- assert len(mock.request_history) == 2
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
+ )
+ data = {"title": "Test Title", "message": "Test Message"}
+ await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
+ assert requests_mock.called
+ assert requests_mock.call_count == 1
- expected_body = {
- "body": "Test Message",
- "device_iden": "identity1",
- "title": "Test Title",
- "type": "note",
- }
- assert mock.request_history[0].json() == expected_body
- expected_body = {
- "body": "Test Message",
- "device_iden": "identity2",
- "title": "Test Title",
- "type": "note",
- }
- assert mock.request_history[1].json() == expected_body
+ expected_body = {"body": "Test Message", "title": "Test Title", "type": "note"}
+ assert requests_mock.last_request.json() == expected_body
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
- )
- def test_pushbullet_push_email(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
- }
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {
- "title": "Test Title",
- "message": "Test Message",
- "target": ["email/user@host.net"],
- }
- self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
- assert mock.called
- assert mock.call_count == 1
- assert len(mock.request_history) == 1
- expected_body = {
- "body": "Test Message",
- "email": "user@host.net",
- "title": "Test Title",
- "type": "note",
+async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- assert mock.request_history[0].json() == expected_body
-
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
)
- def test_pushbullet_push_mixed(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
- }
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {
- "title": "Test Title",
- "message": "Test Message",
- "target": ["device/DESKTOP", "email/user@host.net"],
- }
- self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
- assert mock.called
- assert mock.call_count == 2
- assert len(mock.request_history) == 2
+ data = {
+ "title": "Test Title",
+ "message": "Test Message",
+ "target": ["device/DESKTOP"],
+ }
+ await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
+ assert requests_mock.called
+ assert requests_mock.call_count == 1
- expected_body = {
- "body": "Test Message",
- "device_iden": "identity1",
- "title": "Test Title",
- "type": "note",
- }
- assert mock.request_history[0].json() == expected_body
- expected_body = {
- "body": "Test Message",
- "email": "user@host.net",
- "title": "Test Title",
- "type": "note",
+ expected_body = {
+ "body": "Test Message",
+ "device_iden": "identity1",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.last_request.json() == expected_body
+
+
+async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- assert mock.request_history[1].json() == expected_body
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
+ )
+ data = {
+ "title": "Test Title",
+ "message": "Test Message",
+ "target": ["device/DESKTOP", "device/My iPhone"],
+ }
+ await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
+ assert requests_mock.called
+ assert requests_mock.call_count == 2
+ assert len(requests_mock.request_history) == 2
- @requests_mock.Mocker()
- @patch.object(
- PushBullet,
- "_get_data",
- return_value=json.loads(load_fixture("pushbullet_devices.json")),
+ expected_body = {
+ "body": "Test Message",
+ "device_iden": "identity1",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.request_history[0].json() == expected_body
+ expected_body = {
+ "body": "Test Message",
+ "device_iden": "identity2",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.request_history[1].json() == expected_body
+
+
+async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
+ }
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
)
- def test_pushbullet_push_no_file(self, mock, mock__get_data):
- """Test pushbullet push to default target."""
- config = {
- notify.DOMAIN: {
- "name": "test",
- "platform": "pushbullet",
- "api_key": "MYFAKEKEY",
- }
+ data = {
+ "title": "Test Title",
+ "message": "Test Message",
+ "target": ["email/user@host.net"],
+ }
+ await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
+ assert requests_mock.called
+ assert requests_mock.call_count == 1
+ assert len(requests_mock.request_history) == 1
+
+ expected_body = {
+ "body": "Test Message",
+ "email": "user@host.net",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.request_history[0].json() == expected_body
+
+
+async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- with assert_setup_component(1) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert handle_config[notify.DOMAIN]
- mock.register_uri(
- requests_mock.POST,
- "https://api.pushbullet.com/v2/pushes",
- status_code=200,
- json={"mock_response": "Ok"},
- )
- data = {
- "title": "Test Title",
- "message": "Test Message",
- "target": ["device/DESKTOP", "device/My iPhone"],
- "data": {"file": "not_a_file"},
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
+ )
+ data = {
+ "title": "Test Title",
+ "message": "Test Message",
+ "target": ["device/DESKTOP", "email/user@host.net"],
+ }
+ await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
+ assert requests_mock.called
+ assert requests_mock.call_count == 2
+ assert len(requests_mock.request_history) == 2
+
+ expected_body = {
+ "body": "Test Message",
+ "device_iden": "identity1",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.request_history[0].json() == expected_body
+ expected_body = {
+ "body": "Test Message",
+ "email": "user@host.net",
+ "title": "Test Title",
+ "type": "note",
+ }
+ assert requests_mock.request_history[1].json() == expected_body
+
+
+async def test_pushbullet_push_no_file(hass, requests_mock, mock_pushbullet):
+ """Test pushbullet push to default target."""
+ config = {
+ notify.DOMAIN: {
+ "name": "test",
+ "platform": "pushbullet",
+ "api_key": "MYFAKEKEY",
}
- assert not self.hass.services.call(notify.DOMAIN, "test", data)
- self.hass.block_till_done()
+ }
+ with assert_setup_component(1) as handle_config:
+ assert await async_setup_component(hass, notify.DOMAIN, config)
+ await hass.async_block_till_done()
+ assert handle_config[notify.DOMAIN]
+ requests_mock.register_uri(
+ "POST",
+ "https://api.pushbullet.com/v2/pushes",
+ status_code=200,
+ json={"mock_response": "Ok"},
+ )
+ data = {
+ "title": "Test Title",
+ "message": "Test Message",
+ "target": ["device/DESKTOP", "device/My iPhone"],
+ "data": {"file": "not_a_file"},
+ }
+ assert not await hass.services.async_call(notify.DOMAIN, "test", data)
+ await hass.async_block_till_done()
diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py
index 681b3675aa608c..a16923d0a73d6b 100644
--- a/tests/components/pvpc_hourly_pricing/conftest.py
+++ b/tests/components/pvpc_hourly_pricing/conftest.py
@@ -2,7 +2,11 @@
import pytest
from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN
-from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ CURRENCY_EURO,
+ ENERGY_KILO_WATT_HOUR,
+)
from tests.common import load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -15,7 +19,10 @@
def check_valid_state(state, tariff: str, value=None, key_attr=None):
"""Ensure that sensor has a valid state and attributes."""
assert state
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == f"€/{ENERGY_KILO_WATT_HOUR}"
+ assert (
+ state.attributes[ATTR_UNIT_OF_MEASUREMENT]
+ == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
+ )
try:
_ = float(state.state)
# safety margins for current electricity price (it shouldn't be out of [0, 0.2])
diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py
index 57861b8b72be37..6dae784a0cc879 100644
--- a/tests/components/pvpc_hourly_pricing/test_sensor.py
+++ b/tests/components/pvpc_hourly_pricing/test_sensor.py
@@ -54,12 +54,8 @@ def mock_now():
# sensor has no more prices, state is "unavailable" from now on
await _process_time_step(hass, mock_data, value="unavailable")
await _process_time_step(hass, mock_data, value="unavailable")
- num_errors = sum(
- 1 for x in caplog.get_records("call") if x.levelno == logging.ERROR
- )
- num_warnings = sum(
- 1 for x in caplog.get_records("call") if x.levelno == logging.WARNING
- )
+ num_errors = sum(1 for x in caplog.records if x.levelno == logging.ERROR)
+ num_warnings = sum(1 for x in caplog.records if x.levelno == logging.WARNING)
assert num_warnings == 1
assert num_errors == 0
assert pvpc_aioclient_mock.call_count == 9
diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py
index 96575f811546ae..aa6a2a026799a8 100644
--- a/tests/components/radarr/test_sensor.py
+++ b/tests/components/radarr/test_sensor.py
@@ -1,13 +1,10 @@
"""The tests for the Radarr platform."""
-import unittest
-
import pytest
-import homeassistant.components.radarr.sensor as radarr
from homeassistant.const import DATA_GIGABYTES
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import get_test_home_assistant
def mocked_exception(*args, **kwargs):
@@ -192,32 +189,10 @@ def json(self):
return MockResponse({"error": "Unauthorized"}, 401)
-class TestRadarrSetup(unittest.TestCase):
- """Test the Radarr platform."""
-
- # pylint: disable=invalid-name
- DEVICES = []
-
- def add_entities(self, devices, update):
- """Mock add devices."""
- for device in devices:
- self.DEVICES.append(device)
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.DEVICES = []
- self.hass = get_test_home_assistant()
- self.hass.config.time_zone = "America/Los_Angeles"
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_diskspace_no_paths(self, req_mock):
- """Test getting all disk space."""
- config = {
+async def test_diskspace_no_paths(hass):
+ """Test getting all disk space."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -225,19 +200,28 @@ def test_diskspace_no_paths(self, req_mock):
"include_paths": [],
"monitored_conditions": ["diskspace"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert "263.10" == device.state
- assert "mdi:harddisk" == device.icon
- assert DATA_GIGABYTES == device.unit_of_measurement
- assert "Radarr Disk Space" == device.name
- assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_diskspace_paths(self, req_mock):
- """Test getting diskspace for included paths."""
- config = {
+ }
+
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("sensor.radarr_disk_space")
+ assert entity is not None
+ assert "263.10" == entity.state
+ assert "mdi:harddisk" == entity.attributes["icon"]
+ assert DATA_GIGABYTES == entity.attributes["unit_of_measurement"]
+ assert "Radarr Disk Space" == entity.attributes["friendly_name"]
+ assert "263.10/465.42GB (56.53%)" == entity.attributes["/data"]
+
+
+async def test_diskspace_paths(hass):
+ """Test getting diskspace for included paths."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -245,19 +229,28 @@ def test_diskspace_paths(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["diskspace"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert "263.10" == device.state
- assert "mdi:harddisk" == device.icon
- assert DATA_GIGABYTES == device.unit_of_measurement
- assert "Radarr Disk Space" == device.name
- assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"]
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_commands(self, req_mock):
- """Test getting running commands."""
- config = {
+ }
+
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("sensor.radarr_disk_space")
+ assert entity is not None
+ assert "263.10" == entity.state
+ assert "mdi:harddisk" == entity.attributes["icon"]
+ assert DATA_GIGABYTES == entity.attributes["unit_of_measurement"]
+ assert "Radarr Disk Space" == entity.attributes["friendly_name"]
+ assert "263.10/465.42GB (56.53%)" == entity.attributes["/data"]
+
+
+async def test_commands(hass):
+ """Test getting running commands."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -265,19 +258,28 @@ def test_commands(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["commands"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert 1 == device.state
- assert "mdi:code-braces" == device.icon
- assert "Commands" == device.unit_of_measurement
- assert "Radarr Commands" == device.name
- assert "pending" == device.device_state_attributes["RescanMovie"]
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_movies(self, req_mock):
- """Test getting the number of movies."""
- config = {
+ }
+
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("sensor.radarr_commands")
+ assert entity is not None
+ assert 1 == int(entity.state)
+ assert "mdi:code-braces" == entity.attributes["icon"]
+ assert "Commands" == entity.attributes["unit_of_measurement"]
+ assert "Radarr Commands" == entity.attributes["friendly_name"]
+ assert "pending" == entity.attributes["RescanMovie"]
+
+
+async def test_movies(hass):
+ """Test getting the number of movies."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -285,19 +287,28 @@ def test_movies(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["movies"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert 1 == device.state
- assert "mdi:television" == device.icon
- assert "Movies" == device.unit_of_measurement
- assert "Radarr Movies" == device.name
- assert "false" == device.device_state_attributes["Assassin's Creed (2016)"]
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_upcoming_multiple_days(self, req_mock):
- """Test the upcoming movies for multiple days."""
- config = {
+ }
+
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("sensor.radarr_movies")
+ assert entity is not None
+ assert 1 == int(entity.state)
+ assert "mdi:television" == entity.attributes["icon"]
+ assert "Movies" == entity.attributes["unit_of_measurement"]
+ assert "Radarr Movies" == entity.attributes["friendly_name"]
+ assert "false" == entity.attributes["Assassin's Creed (2016)"]
+
+
+async def test_upcoming_multiple_days(hass):
+ """Test the upcoming movies for multiple days."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -305,26 +316,32 @@ def test_upcoming_multiple_days(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert 1 == device.state
- assert "mdi:television" == device.icon
- assert "Movies" == device.unit_of_measurement
- assert "Radarr Upcoming" == device.name
- assert (
- "2017-01-27T00:00:00Z"
- == device.device_state_attributes["Resident Evil (2017)"]
- )
-
- @pytest.mark.skip
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_upcoming_today(self, req_mock):
- """Test filtering for a single day.
-
- Radarr needs to respond with at least 2 days.
- """
- config = {
+ }
+
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("sensor.radarr_upcoming")
+ assert entity is not None
+ assert 1 == int(entity.state)
+ assert "mdi:television" == entity.attributes["icon"]
+ assert "Movies" == entity.attributes["unit_of_measurement"]
+ assert "Radarr Upcoming" == entity.attributes["friendly_name"]
+ assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+
+
+@pytest.mark.skip
+async def test_upcoming_today(hass):
+ """Test filtering for a single day.
+
+ Radarr needs to respond with at least 2 days.
+ """
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "1",
@@ -332,22 +349,25 @@ def test_upcoming_today(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert 1 == device.state
- assert "mdi:television" == device.icon
- assert "Movies" == device.unit_of_measurement
- assert "Radarr Upcoming" == device.name
- assert (
- "2017-01-27T00:00:00Z"
- == device.device_state_attributes["Resident Evil (2017)"]
- )
-
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_system_status(self, req_mock):
- """Test the getting of the system status."""
- config = {
+ }
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+ entity = hass.states.get("sensor.radarr_upcoming")
+ assert 1 == int(entity.state)
+ assert "mdi:television" == entity.attributes["icon"]
+ assert "Movies" == entity.attributes["unit_of_measurement"]
+ assert "Radarr Upcoming" == entity.attributes["friendly_name"]
+ assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+
+
+async def test_system_status(hass):
+ """Test the getting of the system status."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "2",
@@ -355,19 +375,25 @@ def test_system_status(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["status"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert "0.2.0.210" == device.state
- assert "mdi:information" == device.icon
- assert "Radarr Status" == device.name
- assert "4.8.13.1" == device.device_state_attributes["osVersion"]
-
- @pytest.mark.skip
- @patch("requests.get", side_effect=mocked_requests_get)
- def test_ssl(self, req_mock):
- """Test SSL being enabled."""
- config = {
+ }
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+ entity = hass.states.get("sensor.radarr_status")
+ assert entity is not None
+ assert "0.2.0.210" == entity.state
+ assert "mdi:information" == entity.attributes["icon"]
+ assert "Radarr Status" == entity.attributes["friendly_name"]
+ assert "4.8.13.1" == entity.attributes["osVersion"]
+
+
+async def test_ssl(hass):
+ """Test SSL being enabled."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "1",
@@ -376,23 +402,26 @@ def test_ssl(self, req_mock):
"monitored_conditions": ["upcoming"],
"ssl": "true",
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert 1 == device.state
- assert "s" == device.ssl
- assert "mdi:television" == device.icon
- assert "Movies" == device.unit_of_measurement
- assert "Radarr Upcoming" == device.name
- assert (
- "2017-01-27T00:00:00Z"
- == device.device_state_attributes["Resident Evil (2017)"]
- )
-
- @patch("requests.get", side_effect=mocked_exception)
- def test_exception_handling(self, req_mock):
- """Test exception being handled."""
- config = {
+ }
+ with patch(
+ "requests.get",
+ side_effect=mocked_requests_get,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+ entity = hass.states.get("sensor.radarr_upcoming")
+ assert entity is not None
+ assert 1 == int(entity.state)
+ assert "mdi:television" == entity.attributes["icon"]
+ assert "Movies" == entity.attributes["unit_of_measurement"]
+ assert "Radarr Upcoming" == entity.attributes["friendly_name"]
+ assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+
+
+async def test_exception_handling(hass):
+ """Test exception being handled."""
+ config = {
+ "sensor": {
"platform": "radarr",
"api_key": "foo",
"days": "1",
@@ -400,7 +429,13 @@ def test_exception_handling(self, req_mock):
"include_paths": ["/data"],
"monitored_conditions": ["upcoming"],
}
- radarr.setup_platform(self.hass, config, self.add_entities, None)
- for device in self.DEVICES:
- device.update()
- assert device.state is None
+ }
+ with patch(
+ "requests.get",
+ side_effect=mocked_exception,
+ ):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+ entity = hass.states.get("sensor.radarr_upcoming")
+ assert entity is not None
+ assert "unavailable" == entity.state
diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py
index be4a5fd20fe7ea..28de0da39fa2d2 100644
--- a/tests/components/rainmachine/test_config_flow.py
+++ b/tests/components/rainmachine/test_config_flow.py
@@ -54,7 +54,7 @@ async def test_invalid_password(hass):
side_effect=RainMachineError,
):
result = await flow.async_step_user(user_input=conf)
- assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"}
+ assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
async def test_show_form(hass):
diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py
index 2f15243c71e8e4..85a8e32018c464 100644
--- a/tests/components/random/test_binary_sensor.py
+++ b/tests/components/random/test_binary_sensor.py
@@ -1,45 +1,44 @@
"""The test for the Random binary sensor platform."""
-import unittest
-
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import get_test_home_assistant
-class TestRandomSensor(unittest.TestCase):
+async def test_random_binary_sensor_on(hass):
"""Test the Random binary sensor."""
+ config = {"binary_sensor": {"platform": "random", "name": "test"}}
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- @patch("homeassistant.components.random.binary_sensor.getrandbits", return_value=1)
- def test_random_binary_sensor_on(self, mocked):
- """Test the Random binary sensor."""
- config = {"binary_sensor": {"platform": "random", "name": "test"}}
-
- assert setup_component(self.hass, "binary_sensor", config)
- self.hass.block_till_done()
+ with patch(
+ "homeassistant.components.random.binary_sensor.getrandbits",
+ return_value=1,
+ ):
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ config,
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.test")
+ state = hass.states.get("binary_sensor.test")
- assert state.state == "on"
+ assert state.state == "on"
- @patch(
- "homeassistant.components.random.binary_sensor.getrandbits", return_value=False
- )
- def test_random_binary_sensor_off(self, mocked):
- """Test the Random binary sensor."""
- config = {"binary_sensor": {"platform": "random", "name": "test"}}
- assert setup_component(self.hass, "binary_sensor", config)
- self.hass.block_till_done()
-
- state = self.hass.states.get("binary_sensor.test")
-
- assert state.state == "off"
+async def test_random_binary_sensor_off(hass):
+ """Test the Random binary sensor."""
+ config = {"binary_sensor": {"platform": "random", "name": "test"}}
+
+ with patch(
+ "homeassistant.components.random.binary_sensor.getrandbits",
+ return_value=False,
+ ):
+ assert await async_setup_component(
+ hass,
+ "binary_sensor",
+ config,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+
+ assert state.state == "off"
diff --git a/tests/components/random/test_sensor.py b/tests/components/random/test_sensor.py
index 657efc9a0ccd81..9ac2588ff0afd6 100644
--- a/tests/components/random/test_sensor.py
+++ b/tests/components/random/test_sensor.py
@@ -1,37 +1,26 @@
"""The test for the random number sensor platform."""
-import unittest
+from homeassistant.setup import async_setup_component
-from homeassistant.setup import setup_component
-from tests.common import get_test_home_assistant
-
-
-class TestRandomSensor(unittest.TestCase):
+async def test_random_sensor(hass):
"""Test the Random number sensor."""
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_random_sensor(self):
- """Test the Random number sensor."""
- config = {
- "sensor": {
- "platform": "random",
- "name": "test",
- "minimum": 10,
- "maximum": 20,
- }
+ config = {
+ "sensor": {
+ "platform": "random",
+ "name": "test",
+ "minimum": 10,
+ "maximum": 20,
}
+ }
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ config,
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.test")
+ state = hass.states.get("sensor.test")
- assert int(state.state) <= config["sensor"]["maximum"]
- assert int(state.state) >= config["sensor"]["minimum"]
+ assert int(state.state) <= config["sensor"]["maximum"]
+ assert int(state.state) >= config["sensor"]["minimum"]
diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py
new file mode 100644
index 00000000000000..d91a86402ac52d
--- /dev/null
+++ b/tests/components/recorder/conftest.py
@@ -0,0 +1,24 @@
+"""Common test tools."""
+
+import pytest
+
+from homeassistant.components.recorder.const import DATA_INSTANCE
+
+from tests.common import get_test_home_assistant, init_recorder_component
+
+
+@pytest.fixture
+def hass_recorder():
+ """Home Assistant fixture with in-memory recorder."""
+ hass = get_test_home_assistant()
+
+ def setup_recorder(config=None):
+ """Set up with params."""
+ init_recorder_component(hass, config)
+ hass.start()
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ return hass
+
+ yield setup_recorder
+ hass.stop()
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index 6116b383341d38..fb860348a46b93 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -1,9 +1,6 @@
"""The tests for the Recorder component."""
# pylint: disable=protected-access
from datetime import datetime, timedelta
-import unittest
-
-import pytest
from homeassistant.components.recorder import (
CONFIG_SCHEMA,
@@ -24,99 +21,69 @@
from .common import wait_recording_done
from tests.async_mock import patch
-from tests.common import (
- async_fire_time_changed,
- get_test_home_assistant,
- init_recorder_component,
-)
-
-
-class TestRecorder(unittest.TestCase):
- """Test the recorder module."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- init_recorder_component(self.hass)
- self.hass.start()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
+from tests.common import async_fire_time_changed, get_test_home_assistant
- def test_saving_state(self):
- """Test saving and restoring a state."""
- entity_id = "test.recorder"
- state = "restoring_from_db"
- attributes = {"test_attr": 5, "test_attr_10": "nice"}
- self.hass.states.set(entity_id, state, attributes)
+def test_saving_state(hass, hass_recorder):
+ """Test saving and restoring a state."""
+ hass = hass_recorder()
- wait_recording_done(self.hass)
+ entity_id = "test.recorder"
+ state = "restoring_from_db"
+ attributes = {"test_attr": 5, "test_attr_10": "nice"}
- with session_scope(hass=self.hass) as session:
- db_states = list(session.query(States))
- assert len(db_states) == 1
- assert db_states[0].event_id > 0
- state = db_states[0].to_native()
+ hass.states.set(entity_id, state, attributes)
- assert state == _state_empty_context(self.hass, entity_id)
+ wait_recording_done(hass)
- def test_saving_event(self):
- """Test saving and restoring an event."""
- event_type = "EVENT_TEST"
- event_data = {"test_attr": 5, "test_attr_10": "nice"}
+ with session_scope(hass=hass) as session:
+ db_states = list(session.query(States))
+ assert len(db_states) == 1
+ assert db_states[0].event_id > 0
+ state = db_states[0].to_native()
- events = []
+ assert state == _state_empty_context(hass, entity_id)
- @callback
- def event_listener(event):
- """Record events from eventbus."""
- if event.event_type == event_type:
- events.append(event)
- self.hass.bus.listen(MATCH_ALL, event_listener)
+def test_saving_event(hass, hass_recorder):
+ """Test saving and restoring an event."""
+ hass = hass_recorder()
- self.hass.bus.fire(event_type, event_data)
+ event_type = "EVENT_TEST"
+ event_data = {"test_attr": 5, "test_attr_10": "nice"}
- wait_recording_done(self.hass)
+ events = []
- assert len(events) == 1
- event = events[0]
+ @callback
+ def event_listener(event):
+ """Record events from eventbus."""
+ if event.event_type == event_type:
+ events.append(event)
- self.hass.data[DATA_INSTANCE].block_till_done()
+ hass.bus.listen(MATCH_ALL, event_listener)
- with session_scope(hass=self.hass) as session:
- db_events = list(session.query(Events).filter_by(event_type=event_type))
- assert len(db_events) == 1
- db_event = db_events[0].to_native()
+ hass.bus.fire(event_type, event_data)
- assert event.event_type == db_event.event_type
- assert event.data == db_event.data
- assert event.origin == db_event.origin
+ wait_recording_done(hass)
- # Recorder uses SQLite and stores datetimes as integer unix timestamps
- assert event.time_fired.replace(microsecond=0) == db_event.time_fired.replace(
- microsecond=0
- )
+ assert len(events) == 1
+ event = events[0]
+ hass.data[DATA_INSTANCE].block_till_done()
-@pytest.fixture
-def hass_recorder():
- """Home Assistant fixture with in-memory recorder."""
- hass = get_test_home_assistant()
+ with session_scope(hass=hass) as session:
+ db_events = list(session.query(Events).filter_by(event_type=event_type))
+ assert len(db_events) == 1
+ db_event = db_events[0].to_native()
- def setup_recorder(config=None):
- """Set up with params."""
- init_recorder_component(hass, config)
- hass.start()
- hass.block_till_done()
- hass.data[DATA_INSTANCE].block_till_done()
- return hass
+ assert event.event_type == db_event.event_type
+ assert event.data == db_event.data
+ assert event.origin == db_event.origin
- yield setup_recorder
- hass.stop()
+ # Recorder uses SQLite and stores datetimes as integer unix timestamps
+ assert event.time_fired.replace(microsecond=0) == db_event.time_fired.replace(
+ microsecond=0
+ )
def _add_entities(hass, entity_ids):
@@ -181,7 +148,20 @@ def test_saving_state_incl_entities(hass_recorder):
def test_saving_event_exclude_event_type(hass_recorder):
"""Test saving and restoring an event."""
- hass = hass_recorder({"exclude": {"event_types": "test"}})
+ hass = hass_recorder(
+ {
+ "exclude": {
+ "event_types": [
+ "service_registered",
+ "homeassistant_start",
+ "component_loaded",
+ "core_config_updated",
+ "homeassistant_started",
+ "test",
+ ]
+ }
+ }
+ )
events = _add_events(hass, ["test", "test2"])
assert len(events) == 1
assert events[0].event_type == "test2"
diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py
index bf659282e3e437..26a1e487ba8274 100644
--- a/tests/components/recorder/test_models.py
+++ b/tests/components/recorder/test_models.py
@@ -1,6 +1,5 @@
"""The tests for the Recorder component."""
from datetime import datetime
-import unittest
import pytest
import pytz
@@ -19,151 +18,114 @@
import homeassistant.core as ha
from homeassistant.exceptions import InvalidEntityFormatError
from homeassistant.util import dt
+import homeassistant.util.dt as dt_util
-ENGINE = None
-SESSION = None
+def test_from_event_to_db_event():
+ """Test converting event to db event."""
+ event = ha.Event("test_event", {"some_data": 15})
+ assert event == Events.from_event(event).to_native()
-def setUpModule(): # pylint: disable=invalid-name
- """Set up a database to use."""
- global ENGINE
- global SESSION
- ENGINE = create_engine("sqlite://")
- Base.metadata.create_all(ENGINE)
- session_factory = sessionmaker(bind=ENGINE)
- SESSION = scoped_session(session_factory)
-
-
-def tearDownModule(): # pylint: disable=invalid-name
- """Close database."""
- global ENGINE
- global SESSION
-
- ENGINE.dispose()
- ENGINE = None
- SESSION = None
-
-
-class TestEvents(unittest.TestCase):
- """Test Events model."""
-
- # pylint: disable=no-self-use
- def test_from_event(self):
- """Test converting event to db event."""
- event = ha.Event("test_event", {"some_data": 15})
- assert event == Events.from_event(event).to_native()
+def test_from_event_to_db_state():
+ """Test converting event to db state."""
+ state = ha.State("sensor.temperature", "18")
+ event = ha.Event(
+ EVENT_STATE_CHANGED,
+ {"entity_id": "sensor.temperature", "old_state": None, "new_state": state},
+ context=state.context,
+ )
+ # We don't restore context unless we need it by joining the
+ # events table on the event_id for state_changed events
+ state.context = ha.Context(id=None)
+ assert state == States.from_event(event).to_native()
+
+
+def test_from_event_to_delete_state():
+ """Test converting deleting state event to db state."""
+ event = ha.Event(
+ EVENT_STATE_CHANGED,
+ {
+ "entity_id": "sensor.temperature",
+ "old_state": ha.State("sensor.temperature", "18"),
+ "new_state": None,
+ },
+ )
+ db_state = States.from_event(event)
+
+ assert db_state.entity_id == "sensor.temperature"
+ assert db_state.domain == "sensor"
+ assert db_state.state == ""
+ assert db_state.last_changed == event.time_fired
+ assert db_state.last_updated == event.time_fired
+
+
+def test_entity_ids():
+ """Test if entity ids helper method works."""
+ engine = create_engine("sqlite://")
+ Base.metadata.create_all(engine)
+ session_factory = sessionmaker(bind=engine)
+
+ session = scoped_session(session_factory)
+ session.query(Events).delete()
+ session.query(States).delete()
+ session.query(RecorderRuns).delete()
+
+ run = RecorderRuns(
+ start=datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC),
+ end=datetime(2016, 7, 9, 23, 0, 0, tzinfo=dt.UTC),
+ closed_incorrect=False,
+ created=datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC),
+ )
+ session.add(run)
+ session.commit()
-class TestStates(unittest.TestCase):
- """Test States model."""
+ before_run = datetime(2016, 7, 9, 8, 0, 0, tzinfo=dt.UTC)
+ in_run = datetime(2016, 7, 9, 13, 0, 0, tzinfo=dt.UTC)
+ in_run2 = datetime(2016, 7, 9, 15, 0, 0, tzinfo=dt.UTC)
+ in_run3 = datetime(2016, 7, 9, 18, 0, 0, tzinfo=dt.UTC)
+ after_run = datetime(2016, 7, 9, 23, 30, 0, tzinfo=dt.UTC)
- # pylint: disable=no-self-use
+ assert run.to_native() == run
+ assert run.entity_ids() == []
- def test_from_event(self):
- """Test converting event to db state."""
- state = ha.State("sensor.temperature", "18")
- event = ha.Event(
- EVENT_STATE_CHANGED,
- {"entity_id": "sensor.temperature", "old_state": None, "new_state": state},
- context=state.context,
- )
- # We don't restore context unless we need it by joining the
- # events table on the event_id for state_changed events
- state.context = ha.Context(id=None)
- assert state == States.from_event(event).to_native()
-
- def test_from_event_to_delete_state(self):
- """Test converting deleting state event to db state."""
- event = ha.Event(
- EVENT_STATE_CHANGED,
- {
- "entity_id": "sensor.temperature",
- "old_state": ha.State("sensor.temperature", "18"),
- "new_state": None,
- },
- )
- db_state = States.from_event(event)
-
- assert db_state.entity_id == "sensor.temperature"
- assert db_state.domain == "sensor"
- assert db_state.state == ""
- assert db_state.last_changed == event.time_fired
- assert db_state.last_updated == event.time_fired
-
-
-class TestRecorderRuns(unittest.TestCase):
- """Test recorder run model."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up recorder runs."""
- self.session = session = SESSION()
- session.query(Events).delete()
- session.query(States).delete()
- session.query(RecorderRuns).delete()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Clean up."""
- self.session.rollback()
-
- def test_entity_ids(self):
- """Test if entity ids helper method works."""
- run = RecorderRuns(
- start=datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC),
- end=datetime(2016, 7, 9, 23, 0, 0, tzinfo=dt.UTC),
- closed_incorrect=False,
- created=datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC),
+ session.add(
+ States(
+ entity_id="sensor.temperature",
+ state="20",
+ last_changed=before_run,
+ last_updated=before_run,
)
-
- self.session.add(run)
- self.session.commit()
-
- before_run = datetime(2016, 7, 9, 8, 0, 0, tzinfo=dt.UTC)
- in_run = datetime(2016, 7, 9, 13, 0, 0, tzinfo=dt.UTC)
- in_run2 = datetime(2016, 7, 9, 15, 0, 0, tzinfo=dt.UTC)
- in_run3 = datetime(2016, 7, 9, 18, 0, 0, tzinfo=dt.UTC)
- after_run = datetime(2016, 7, 9, 23, 30, 0, tzinfo=dt.UTC)
-
- assert run.to_native() == run
- assert run.entity_ids() == []
-
- self.session.add(
- States(
- entity_id="sensor.temperature",
- state="20",
- last_changed=before_run,
- last_updated=before_run,
- )
- )
- self.session.add(
- States(
- entity_id="sensor.sound",
- state="10",
- last_changed=after_run,
- last_updated=after_run,
- )
+ )
+ session.add(
+ States(
+ entity_id="sensor.sound",
+ state="10",
+ last_changed=after_run,
+ last_updated=after_run,
)
+ )
- self.session.add(
- States(
- entity_id="sensor.humidity",
- state="76",
- last_changed=in_run,
- last_updated=in_run,
- )
+ session.add(
+ States(
+ entity_id="sensor.humidity",
+ state="76",
+ last_changed=in_run,
+ last_updated=in_run,
)
- self.session.add(
- States(
- entity_id="sensor.lux",
- state="5",
- last_changed=in_run3,
- last_updated=in_run3,
- )
+ )
+ session.add(
+ States(
+ entity_id="sensor.lux",
+ state="5",
+ last_changed=in_run3,
+ last_updated=in_run3,
)
+ )
- assert sorted(run.entity_ids()) == ["sensor.humidity", "sensor.lux"]
- assert run.entity_ids(in_run2) == ["sensor.humidity"]
+ assert sorted(run.entity_ids()) == ["sensor.humidity", "sensor.lux"]
+ assert run.entity_ids(in_run2) == ["sensor.humidity"]
def test_states_from_native_invalid_entity_id():
@@ -241,3 +203,16 @@ async def test_process_timestamp_to_utc_isoformat():
== "2016-07-09T21:31:00+00:00"
)
assert process_timestamp_to_utc_isoformat(None) is None
+
+
+async def test_event_to_db_model():
+ """Test we can round trip Event conversion."""
+ event = ha.Event(
+ "state_changed", {"some": "attr"}, ha.EventOrigin.local, dt_util.utcnow()
+ )
+ native = Events.from_event(event).to_native()
+ assert native == event
+
+ native = Events.from_event(event, event_data="{}").to_native()
+ event.data = {}
+ assert native == event
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index a93e3537905dbf..91a2299e4b619a 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -1,7 +1,6 @@
"""Test data purging."""
from datetime import datetime, timedelta
import json
-import unittest
from homeassistant.components import recorder
from homeassistant.components.recorder.const import DATA_INSTANCE
@@ -13,226 +12,216 @@
from .common import wait_recording_done
from tests.async_mock import patch
-from tests.common import get_test_home_assistant, init_recorder_component
-
-
-class TestRecorderPurge(unittest.TestCase):
- """Base class for common recorder tests."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- init_recorder_component(self.hass)
- self.hass.start()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- def _add_test_states(self):
- """Add multiple states to the db for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
- attributes = {"test_attr": 5, "test_attr_10": "nice"}
-
- self.hass.block_till_done()
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- with recorder.session_scope(hass=self.hass) as session:
- for event_id in range(6):
- if event_id < 2:
- timestamp = eleven_days_ago
- state = "autopurgeme"
- elif event_id < 4:
- timestamp = five_days_ago
- state = "purgeme"
- else:
- timestamp = now
- state = "dontpurgeme"
-
- session.add(
- States(
- entity_id="test.recorder2",
- domain="sensor",
- state=state,
- attributes=json.dumps(attributes),
- last_changed=timestamp,
- last_updated=timestamp,
- created=timestamp,
- event_id=event_id + 1000,
- )
- )
- def _add_test_events(self):
- """Add a few events for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
- event_data = {"test_attr": 5, "test_attr_10": "nice"}
-
- self.hass.block_till_done()
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- with recorder.session_scope(hass=self.hass) as session:
- for event_id in range(6):
- if event_id < 2:
- timestamp = eleven_days_ago
- event_type = "EVENT_TEST_AUTOPURGE"
- elif event_id < 4:
- timestamp = five_days_ago
- event_type = "EVENT_TEST_PURGE"
- else:
- timestamp = now
- event_type = "EVENT_TEST"
-
- session.add(
- Events(
- event_type=event_type,
- event_data=json.dumps(event_data),
- origin="LOCAL",
- created=timestamp,
- time_fired=timestamp,
- )
- )
- def _add_test_recorder_runs(self):
- """Add a few recorder_runs for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
-
- self.hass.block_till_done()
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- with recorder.session_scope(hass=self.hass) as session:
- for rec_id in range(6):
- if rec_id < 2:
- timestamp = eleven_days_ago
- elif rec_id < 4:
- timestamp = five_days_ago
- else:
- timestamp = now
-
- session.add(
- RecorderRuns(
- start=timestamp,
- created=dt_util.utcnow(),
- end=timestamp + timedelta(days=1),
- )
+def test_purge_old_states(hass, hass_recorder):
+ """Test deleting old states."""
+ hass = hass_recorder()
+ _add_test_states(hass)
+
+ # make sure we start with 6 states
+ with session_scope(hass=hass) as session:
+ states = session.query(States)
+ assert states.count() == 6
+
+ # run purge_old_data()
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert not finished
+ assert states.count() == 4
+
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert not finished
+ assert states.count() == 2
+
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert finished
+ assert states.count() == 2
+
+
+def test_purge_old_events(hass, hass_recorder):
+ """Test deleting old events."""
+ hass = hass_recorder()
+ _add_test_events(hass)
+
+ with session_scope(hass=hass) as session:
+ events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
+ assert events.count() == 6
+
+ # run purge_old_data()
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert not finished
+ assert events.count() == 4
+
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert not finished
+ assert events.count() == 2
+
+ # we should only have 2 events left
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ assert finished
+ assert events.count() == 2
+
+
+def test_purge_method(hass, hass_recorder):
+ """Test purge method."""
+ hass = hass_recorder()
+ service_data = {"keep_days": 4}
+ _add_test_events(hass)
+ _add_test_states(hass)
+ _add_test_recorder_runs(hass)
+
+ # make sure we start with 6 states
+ with session_scope(hass=hass) as session:
+ states = session.query(States)
+ assert states.count() == 6
+
+ events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
+ assert events.count() == 6
+
+ recorder_runs = session.query(RecorderRuns)
+ assert recorder_runs.count() == 7
+
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ # run purge method - no service data, use defaults
+ hass.services.call("recorder", "purge")
+ hass.block_till_done()
+
+ # Small wait for recorder thread
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ # only purged old events
+ assert states.count() == 4
+ assert events.count() == 4
+
+ # run purge method - correct service data
+ hass.services.call("recorder", "purge", service_data=service_data)
+ hass.block_till_done()
+
+ # Small wait for recorder thread
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ # we should only have 2 states left after purging
+ assert states.count() == 2
+
+ # now we should only have 2 events left
+ assert events.count() == 2
+
+ # now we should only have 3 recorder runs left
+ assert recorder_runs.count() == 3
+
+ assert not ("EVENT_TEST_PURGE" in (event.event_type for event in events.all()))
+
+ # run purge method - correct service data, with repack
+ with patch("homeassistant.components.recorder.purge._LOGGER") as mock_logger:
+ service_data["repack"] = True
+ hass.services.call("recorder", "purge", service_data=service_data)
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+ assert (
+ mock_logger.debug.mock_calls[5][1][0]
+ == "Vacuuming SQL DB to free space"
+ )
+
+
+def _add_test_states(hass):
+ """Add multiple states to the db for testing."""
+ now = datetime.now()
+ five_days_ago = now - timedelta(days=5)
+ eleven_days_ago = now - timedelta(days=11)
+ attributes = {"test_attr": 5, "test_attr_10": "nice"}
+
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ with recorder.session_scope(hass=hass) as session:
+ for event_id in range(6):
+ if event_id < 2:
+ timestamp = eleven_days_ago
+ state = "autopurgeme"
+ elif event_id < 4:
+ timestamp = five_days_ago
+ state = "purgeme"
+ else:
+ timestamp = now
+ state = "dontpurgeme"
+
+ session.add(
+ States(
+ entity_id="test.recorder2",
+ domain="sensor",
+ state=state,
+ attributes=json.dumps(attributes),
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ event_id=event_id + 1000,
)
+ )
- def test_purge_old_states(self):
- """Test deleting old states."""
- self._add_test_states()
- # make sure we start with 6 states
- with session_scope(hass=self.hass) as session:
- states = session.query(States)
- assert states.count() == 6
-
- # run purge_old_data()
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert states.count() == 4
-
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert states.count() == 2
-
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert finished
- assert states.count() == 2
-
- def test_purge_old_events(self):
- """Test deleting old events."""
- self._add_test_events()
-
- with session_scope(hass=self.hass) as session:
- events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
- assert events.count() == 6
-
- # run purge_old_data()
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert events.count() == 4
-
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert events.count() == 2
-
- # we should only have 2 events left
- finished = purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- assert finished
- assert events.count() == 2
-
- def test_purge_method(self):
- """Test purge method."""
- service_data = {"keep_days": 4}
- self._add_test_events()
- self._add_test_states()
- self._add_test_recorder_runs()
-
- # make sure we start with 6 states
- with session_scope(hass=self.hass) as session:
- states = session.query(States)
- assert states.count() == 6
-
- events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
- assert events.count() == 6
-
- recorder_runs = session.query(RecorderRuns)
- assert recorder_runs.count() == 7
-
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- # run purge method - no service data, use defaults
- self.hass.services.call("recorder", "purge")
- self.hass.block_till_done()
-
- # Small wait for recorder thread
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- # only purged old events
- assert states.count() == 4
- assert events.count() == 4
-
- # run purge method - correct service data
- self.hass.services.call("recorder", "purge", service_data=service_data)
- self.hass.block_till_done()
-
- # Small wait for recorder thread
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
-
- # we should only have 2 states left after purging
- assert states.count() == 2
-
- # now we should only have 2 events left
- assert events.count() == 2
-
- # now we should only have 3 recorder runs left
- assert recorder_runs.count() == 3
-
- assert not (
- "EVENT_TEST_PURGE" in (event.event_type for event in events.all())
+
+def _add_test_events(hass):
+ """Add a few events for testing."""
+ now = datetime.now()
+ five_days_ago = now - timedelta(days=5)
+ eleven_days_ago = now - timedelta(days=11)
+ event_data = {"test_attr": 5, "test_attr_10": "nice"}
+
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ with recorder.session_scope(hass=hass) as session:
+ for event_id in range(6):
+ if event_id < 2:
+ timestamp = eleven_days_ago
+ event_type = "EVENT_TEST_AUTOPURGE"
+ elif event_id < 4:
+ timestamp = five_days_ago
+ event_type = "EVENT_TEST_PURGE"
+ else:
+ timestamp = now
+ event_type = "EVENT_TEST"
+
+ session.add(
+ Events(
+ event_type=event_type,
+ event_data=json.dumps(event_data),
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
)
- # run purge method - correct service data, with repack
- with patch(
- "homeassistant.components.recorder.purge._LOGGER"
- ) as mock_logger:
- service_data["repack"] = True
- self.hass.services.call("recorder", "purge", service_data=service_data)
- self.hass.block_till_done()
- self.hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(self.hass)
- assert (
- mock_logger.debug.mock_calls[5][1][0]
- == "Vacuuming SQL DB to free space"
+
+def _add_test_recorder_runs(hass):
+ """Add a few recorder_runs for testing."""
+ now = datetime.now()
+ five_days_ago = now - timedelta(days=5)
+ eleven_days_ago = now - timedelta(days=11)
+
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ wait_recording_done(hass)
+
+ with recorder.session_scope(hass=hass) as session:
+ for rec_id in range(6):
+ if rec_id < 2:
+ timestamp = eleven_days_ago
+ elif rec_id < 4:
+ timestamp = five_days_ago
+ else:
+ timestamp = now
+
+ session.add(
+ RecorderRuns(
+ start=timestamp,
+ created=dt_util.utcnow(),
+ end=timestamp + timedelta(days=1),
)
+ )
diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py
index faa3679f492dfa..c49abd4038a230 100644
--- a/tests/components/reddit/test_sensor.py
+++ b/tests/components/reddit/test_sensor.py
@@ -1,6 +1,5 @@
"""The tests for the Reddit platform."""
import copy
-import unittest
from homeassistant.components.reddit.sensor import (
ATTR_BODY,
@@ -22,10 +21,9 @@
CONF_PASSWORD,
CONF_USERNAME,
)
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import get_test_home_assistant
VALID_CONFIG = {
"sensor": {
@@ -151,46 +149,36 @@ def _return_data(self, limit):
return data["results"][:limit]
-class TestRedditSetup(unittest.TestCase):
- """Test the Reddit platform."""
+@patch("praw.Reddit", new=MockPraw)
+async def test_setup_with_valid_config(hass):
+ """Test the platform setup with Reddit configuration."""
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
+ state = hass.states.get("sensor.reddit_worldnews")
+ assert int(state.state) == MOCK_RESULTS_LENGTH
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
+ state = hass.states.get("sensor.reddit_news")
+ assert int(state.state) == MOCK_RESULTS_LENGTH
- @patch("praw.Reddit", new=MockPraw)
- def test_setup_with_valid_config(self):
- """Test the platform setup with Reddit configuration."""
- setup_component(self.hass, "sensor", VALID_CONFIG)
- self.hass.block_till_done()
+ assert state.attributes[ATTR_SUBREDDIT] == "news"
- state = self.hass.states.get("sensor.reddit_worldnews")
- assert int(state.state) == MOCK_RESULTS_LENGTH
-
- state = self.hass.states.get("sensor.reddit_news")
- assert int(state.state) == MOCK_RESULTS_LENGTH
-
- assert state.attributes[ATTR_SUBREDDIT] == "news"
+ assert state.attributes[ATTR_POSTS][0] == {
+ ATTR_ID: 0,
+ ATTR_URL: "http://example.com/1",
+ ATTR_TITLE: "example1",
+ ATTR_SCORE: "1",
+ ATTR_COMMENTS_NUMBER: "1",
+ ATTR_CREATED: "",
+ ATTR_BODY: "example1 selftext",
+ }
- assert state.attributes[ATTR_POSTS][0] == {
- ATTR_ID: 0,
- ATTR_URL: "http://example.com/1",
- ATTR_TITLE: "example1",
- ATTR_SCORE: "1",
- ATTR_COMMENTS_NUMBER: "1",
- ATTR_CREATED: "",
- ATTR_BODY: "example1 selftext",
- }
+ assert state.attributes[CONF_SORT_BY] == "hot"
- assert state.attributes[CONF_SORT_BY] == "hot"
- @patch("praw.Reddit", new=MockPraw)
- def test_setup_with_invalid_config(self):
- """Test the platform setup with invalid Reddit configuration."""
- setup_component(self.hass, "sensor", INVALID_SORT_BY_CONFIG)
- assert not self.hass.states.get("sensor.reddit_worldnews")
+@patch("praw.Reddit", new=MockPraw)
+async def test_setup_with_invalid_config(hass):
+ """Test the platform setup with invalid Reddit configuration."""
+ assert await async_setup_component(hass, "sensor", INVALID_SORT_BY_CONFIG)
+ await hass.async_block_till_done()
+ assert not hass.states.get("sensor.reddit_worldnews")
diff --git a/tests/components/remember_the_milk/const.py b/tests/components/remember_the_milk/const.py
new file mode 100644
index 00000000000000..8423c7f4651b59
--- /dev/null
+++ b/tests/components/remember_the_milk/const.py
@@ -0,0 +1,14 @@
+"""Constants for remember_the_milk tests."""
+
+import json
+
+PROFILE = "myprofile"
+TOKEN = "mytoken"
+JSON_STRING = json.dumps(
+ {
+ "myprofile": {
+ "token": "mytoken",
+ "id_map": {"1234": {"list_id": "0", "timeseries_id": "1", "task_id": "2"}},
+ }
+ }
+)
diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py
index 1c9438c602b79d..a743f61057d111 100644
--- a/tests/components/remember_the_milk/test_init.py
+++ b/tests/components/remember_the_milk/test_init.py
@@ -1,89 +1,66 @@
"""Tests for the Remember The Milk component."""
-import json
import logging
-import unittest
import homeassistant.components.remember_the_milk as rtm
+from .const import JSON_STRING, PROFILE, TOKEN
+
from tests.async_mock import Mock, mock_open, patch
-from tests.common import get_test_home_assistant
_LOGGER = logging.getLogger(__name__)
-class TestConfiguration(unittest.TestCase):
- """Basic tests for the class RememberTheMilkConfiguration."""
-
- def setUp(self):
- """Set up test Home Assistant main loop."""
- self.hass = get_test_home_assistant()
- self.profile = "myprofile"
- self.token = "mytoken"
- self.json_string = json.dumps(
- {
- "myprofile": {
- "token": "mytoken",
- "id_map": {
- "1234": {"list_id": "0", "timeseries_id": "1", "task_id": "2"}
- },
- }
- }
- )
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Exit Home Assistant."""
- self.hass.stop()
-
- def test_create_new(self):
- """Test creating a new config file."""
- with patch("builtins.open", mock_open()), patch(
- "os.path.isfile", Mock(return_value=False)
- ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"):
- config = rtm.RememberTheMilkConfiguration(self.hass)
- config.set_token(self.profile, self.token)
- assert config.get_token(self.profile) == self.token
-
- def test_load_config(self):
- """Test loading an existing token from the file."""
- with patch("builtins.open", mock_open(read_data=self.json_string)), patch(
- "os.path.isfile", Mock(return_value=True)
- ):
- config = rtm.RememberTheMilkConfiguration(self.hass)
- assert config.get_token(self.profile) == self.token
-
- def test_invalid_data(self):
- """Test starts with invalid data and should not raise an exception."""
- with patch("builtins.open", mock_open(read_data="random characters")), patch(
- "os.path.isfile", Mock(return_value=True)
- ):
- config = rtm.RememberTheMilkConfiguration(self.hass)
- assert config is not None
-
- def test_id_map(self):
- """Test the hass to rtm task is mapping."""
- hass_id = "hass-id-1234"
- list_id = "mylist"
- timeseries_id = "my_timeseries"
- rtm_id = "rtm-id-4567"
- with patch("builtins.open", mock_open()), patch(
- "os.path.isfile", Mock(return_value=False)
- ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"):
- config = rtm.RememberTheMilkConfiguration(self.hass)
-
- assert config.get_rtm_id(self.profile, hass_id) is None
- config.set_rtm_id(self.profile, hass_id, list_id, timeseries_id, rtm_id)
- assert (list_id, timeseries_id, rtm_id) == config.get_rtm_id(
- self.profile, hass_id
- )
- config.delete_rtm_id(self.profile, hass_id)
- assert config.get_rtm_id(self.profile, hass_id) is None
-
- def test_load_key_map(self):
- """Test loading an existing key map from the file."""
- with patch("builtins.open", mock_open(read_data=self.json_string)), patch(
- "os.path.isfile", Mock(return_value=True)
- ):
- config = rtm.RememberTheMilkConfiguration(self.hass)
- assert ("0", "1", "2") == config.get_rtm_id(self.profile, "1234")
+def test_create_new(hass):
+ """Test creating a new config file."""
+ with patch("builtins.open", mock_open()), patch(
+ "os.path.isfile", Mock(return_value=False)
+ ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"):
+ config = rtm.RememberTheMilkConfiguration(hass)
+ config.set_token(PROFILE, TOKEN)
+ assert config.get_token(PROFILE) == TOKEN
+
+
+def test_load_config(hass):
+ """Test loading an existing token from the file."""
+ with patch("builtins.open", mock_open(read_data=JSON_STRING)), patch(
+ "os.path.isfile", Mock(return_value=True)
+ ):
+ config = rtm.RememberTheMilkConfiguration(hass)
+ assert config.get_token(PROFILE) == TOKEN
+
+
+def test_invalid_data(hass):
+ """Test starts with invalid data and should not raise an exception."""
+ with patch("builtins.open", mock_open(read_data="random characters")), patch(
+ "os.path.isfile", Mock(return_value=True)
+ ):
+ config = rtm.RememberTheMilkConfiguration(hass)
+ assert config is not None
+
+
+def test_id_map(hass):
+ """Test the hass to rtm task is mapping."""
+ hass_id = "hass-id-1234"
+ list_id = "mylist"
+ timeseries_id = "my_timeseries"
+ rtm_id = "rtm-id-4567"
+ with patch("builtins.open", mock_open()), patch(
+ "os.path.isfile", Mock(return_value=False)
+ ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"):
+ config = rtm.RememberTheMilkConfiguration(hass)
+
+ assert config.get_rtm_id(PROFILE, hass_id) is None
+ config.set_rtm_id(PROFILE, hass_id, list_id, timeseries_id, rtm_id)
+ assert (list_id, timeseries_id, rtm_id) == config.get_rtm_id(PROFILE, hass_id)
+ config.delete_rtm_id(PROFILE, hass_id)
+ assert config.get_rtm_id(PROFILE, hass_id) is None
+
+
+def test_load_key_map(hass):
+ """Test loading an existing key map from the file."""
+ with patch("builtins.open", mock_open(read_data=JSON_STRING)), patch(
+ "os.path.isfile", Mock(return_value=True)
+ ):
+ config = rtm.RememberTheMilkConfiguration(hass)
+ assert ("0", "1", "2") == config.get_rtm_id(PROFILE, "1234")
diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py
index e56342d861ff28..b18d8f300cfbbe 100644
--- a/tests/components/rest/test_binary_sensor.py
+++ b/tests/components/rest/test_binary_sensor.py
@@ -9,7 +9,7 @@
import homeassistant.components.binary_sensor as binary_sensor
import homeassistant.components.rest.binary_sensor as rest
-from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.const import CONTENT_TYPE_JSON, STATE_OFF, STATE_ON
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import template
from homeassistant.setup import setup_component
@@ -143,7 +143,7 @@ def test_setup_get(self, mock_req):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -170,7 +170,7 @@ def test_setup_post(self, mock_req):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index 4351239064ae40..5ffa12c6167203 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -12,7 +12,12 @@
from homeassistant import config as hass_config
import homeassistant.components.rest.sensor as rest
import homeassistant.components.sensor as sensor
-from homeassistant.const import DATA_MEGABYTES, SERVICE_RELOAD
+from homeassistant.const import (
+ CONTENT_TYPE_JSON,
+ CONTENT_TYPE_TEXT_PLAIN,
+ DATA_MEGABYTES,
+ SERVICE_RELOAD,
+)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.config_validation import template
from homeassistant.setup import async_setup_component, setup_component
@@ -135,7 +140,7 @@ def test_setup_get(self, mock_req):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -164,7 +169,7 @@ def test_setup_post(self, mock_req):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -212,7 +217,7 @@ def setUp(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "' + self.initial_state + '" }',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.name = "foo"
@@ -276,7 +281,7 @@ def test_update_when_value_changed(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "updated_state" }',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor.update()
@@ -288,7 +293,7 @@ def test_update_with_no_template(self):
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
- "plain_state", CaseInsensitiveDict({"Content-Type": "application/json"})
+ "plain_state", CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON})
),
)
self.sensor = rest.RestSensor(
@@ -313,7 +318,7 @@ def test_update_with_json_attrs(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "some_json_value" }',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor = rest.RestSensor(
@@ -337,7 +342,7 @@ def test_update_with_json_attrs_list_dict(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'[{ "key": "another_value" }]',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor = rest.RestSensor(
@@ -361,7 +366,7 @@ def test_update_with_json_attrs_no_data(self, mock_logger):
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
- None, CaseInsensitiveDict({"Content-Type": "application/json"})
+ None, CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON})
),
)
self.sensor = rest.RestSensor(
@@ -387,7 +392,7 @@ def test_update_with_json_attrs_not_dict(self, mock_logger):
"rest.RestData.update",
side_effect=self.update_side_effect(
'["list", "of", "things"]',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor = rest.RestSensor(
@@ -413,7 +418,7 @@ def test_update_with_json_attrs_bad_JSON(self, mock_logger):
"rest.RestData.update",
side_effect=self.update_side_effect(
"This is text rather than JSON data.",
- CaseInsensitiveDict({"Content-Type": "text/plain"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_TEXT_PLAIN}),
),
)
self.sensor = rest.RestSensor(
@@ -439,7 +444,7 @@ def test_update_with_json_attrs_and_template(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "json_state_updated_value" }',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor = rest.RestSensor(
@@ -471,7 +476,7 @@ def test_update_with_json_attrs_with_json_attrs_path(self):
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }',
- CaseInsensitiveDict({"Content-Type": "application/json"}),
+ CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}),
),
)
self.sensor = rest.RestSensor(
diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py
index 065645fffd1e43..47eec8e700fe3b 100644
--- a/tests/components/rest/test_switch.py
+++ b/tests/components/rest/test_switch.py
@@ -123,7 +123,7 @@ def test_setup_with_state_resource(self, aioclient_mock):
CONF_NAME: "foo",
CONF_RESOURCE: "http://localhost",
rest.CONF_STATE_RESOURCE: "http://localhost/state",
- CONF_HEADERS: {"Content-type": "application/json"},
+ CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON},
rest.CONF_BODY_ON: "custom on text",
rest.CONF_BODY_OFF: "custom off text",
}
@@ -143,7 +143,7 @@ def setup_method(self):
self.method = "post"
self.resource = "http://localhost/"
self.state_resource = self.resource
- self.headers = {"Content-type": "application/json"}
+ self.headers = {"Content-type": CONTENT_TYPE_JSON}
self.auth = None
self.body_on = Template("on", self.hass)
self.body_off = Template("off", self.hass)
diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py
index 0aee8ccfbccf01..80ede61be84406 100644
--- a/tests/components/rest_command/test_init.py
+++ b/tests/components/rest_command/test_init.py
@@ -4,6 +4,7 @@
import aiohttp
import homeassistant.components.rest_command as rc
+from homeassistant.const import CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN
from homeassistant.setup import setup_component
from tests.common import assert_setup_component, get_test_home_assistant
@@ -218,27 +219,27 @@ def test_rest_command_headers(self, aioclient_mock):
header_config_variations = {
rc.DOMAIN: {
"no_headers_test": {},
- "content_type_test": {"content_type": "text/plain"},
+ "content_type_test": {"content_type": CONTENT_TYPE_TEXT_PLAIN},
"headers_test": {
"headers": {
- "Accept": "application/json",
+ "Accept": CONTENT_TYPE_JSON,
"User-Agent": "Mozilla/5.0",
}
},
"headers_and_content_type_test": {
- "headers": {"Accept": "application/json"},
- "content_type": "text/plain",
+ "headers": {"Accept": CONTENT_TYPE_JSON},
+ "content_type": CONTENT_TYPE_TEXT_PLAIN,
},
"headers_and_content_type_override_test": {
"headers": {
- "Accept": "application/json",
+ "Accept": CONTENT_TYPE_JSON,
aiohttp.hdrs.CONTENT_TYPE: "application/pdf",
},
- "content_type": "text/plain",
+ "content_type": CONTENT_TYPE_TEXT_PLAIN,
},
"headers_template_test": {
"headers": {
- "Accept": "application/json",
+ "Accept": CONTENT_TYPE_JSON,
"User-Agent": "Mozilla/{{ 3 + 2 }}.0",
}
},
@@ -285,33 +286,33 @@ def test_rest_command_headers(self, aioclient_mock):
assert len(aioclient_mock.mock_calls[1][3]) == 1
assert (
aioclient_mock.mock_calls[1][3].get(aiohttp.hdrs.CONTENT_TYPE)
- == "text/plain"
+ == CONTENT_TYPE_TEXT_PLAIN
)
# headers_test
assert len(aioclient_mock.mock_calls[2][3]) == 2
- assert aioclient_mock.mock_calls[2][3].get("Accept") == "application/json"
+ assert aioclient_mock.mock_calls[2][3].get("Accept") == CONTENT_TYPE_JSON
assert aioclient_mock.mock_calls[2][3].get("User-Agent") == "Mozilla/5.0"
# headers_and_content_type_test
assert len(aioclient_mock.mock_calls[3][3]) == 2
assert (
aioclient_mock.mock_calls[3][3].get(aiohttp.hdrs.CONTENT_TYPE)
- == "text/plain"
+ == CONTENT_TYPE_TEXT_PLAIN
)
- assert aioclient_mock.mock_calls[3][3].get("Accept") == "application/json"
+ assert aioclient_mock.mock_calls[3][3].get("Accept") == CONTENT_TYPE_JSON
# headers_and_content_type_override_test
assert len(aioclient_mock.mock_calls[4][3]) == 2
assert (
aioclient_mock.mock_calls[4][3].get(aiohttp.hdrs.CONTENT_TYPE)
- == "text/plain"
+ == CONTENT_TYPE_TEXT_PLAIN
)
- assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json"
+ assert aioclient_mock.mock_calls[4][3].get("Accept") == CONTENT_TYPE_JSON
# headers_template_test
assert len(aioclient_mock.mock_calls[5][3]) == 2
- assert aioclient_mock.mock_calls[5][3].get("Accept") == "application/json"
+ assert aioclient_mock.mock_calls[5][3].get("Accept") == CONTENT_TYPE_JSON
assert aioclient_mock.mock_calls[5][3].get("User-Agent") == "Mozilla/5.0"
# headers_and_content_type_override_template_test
diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py
index 1468037b70d384..d18076a372adac 100644
--- a/tests/components/rflink/test_sensor.py
+++ b/tests/components/rflink/test_sensor.py
@@ -12,7 +12,12 @@
EVENT_KEY_SENSOR,
TMP_ENTITY,
)
-from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ PERCENTAGE,
+ STATE_UNKNOWN,
+ TEMP_CELSIUS,
+)
from tests.components.rflink.test_init import mock_rflink
@@ -42,7 +47,7 @@ async def test_default_setup(hass, monkeypatch):
config_sensor = hass.states.get("sensor.test")
assert config_sensor
assert config_sensor.state == "unknown"
- assert config_sensor.attributes["unit_of_measurement"] == TEMP_CELSIUS
+ assert config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
# test event for config sensor
event_callback(
@@ -62,7 +67,7 @@ async def test_default_setup(hass, monkeypatch):
new_sensor = hass.states.get("sensor.test2")
assert new_sensor
assert new_sensor.state == "0"
- assert new_sensor.attributes["unit_of_measurement"] == TEMP_CELSIUS
+ assert new_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert new_sensor.attributes["icon"] == "mdi:thermometer"
@@ -160,7 +165,7 @@ async def test_aliases(hass, monkeypatch):
updated_sensor = hass.states.get("sensor.test_02")
assert updated_sensor
assert updated_sensor.state == "65"
- assert updated_sensor.attributes["unit_of_measurement"] == PERCENTAGE
+ assert updated_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
async def test_race_condition(hass, monkeypatch):
diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py
index 1eb39f006911d5..82c4bd7aacd827 100644
--- a/tests/components/rfxtrx/conftest.py
+++ b/tests/components/rfxtrx/conftest.py
@@ -4,11 +4,23 @@
import pytest
from homeassistant.components import rfxtrx
-from homeassistant.setup import async_setup_component
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.util.dt import utcnow
from tests.async_mock import patch
-from tests.common import async_fire_time_changed
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None):
+ """Create rfxtrx config entry data."""
+ return {
+ "device": device,
+ "host": None,
+ "port": None,
+ "automatic_add": automatic_add,
+ "debug": False,
+ "devices": devices,
+ }
@pytest.fixture(autouse=True, name="rfxtrx")
@@ -37,12 +49,12 @@ async def _signal_event(packet_id):
@pytest.fixture(name="rfxtrx_automatic")
async def rfxtrx_automatic_fixture(hass, rfxtrx):
"""Fixture that starts up with automatic additions."""
+ entry_data = create_rfx_test_cfg(automatic_add=True, devices={})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "automatic_add": True}},
- )
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
yield rfxtrx
diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py
index ee757192aafc3e..a52b390395ab3d 100644
--- a/tests/components/rfxtrx/test_binary_sensor.py
+++ b/tests/components/rfxtrx/test_binary_sensor.py
@@ -1,11 +1,12 @@
"""The tests for the Rfxtrx sensor platform."""
import pytest
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import ATTR_EVENT
from homeassistant.core import State
-from homeassistant.setup import async_setup_component
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670"
EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770"
@@ -21,11 +22,12 @@
async def test_one(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}},
- )
+ entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.ac_213c7f2_48")
@@ -36,22 +38,20 @@ async def test_one(hass, rfxtrx):
async def test_one_pt2262(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0913000022670e013970": {
- "data_bits": 4,
- "command_on": 0xE,
- "command_off": 0x7,
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0913000022670e013970": {
+ "data_bits": 4,
+ "command_on": 0xE,
+ "command_off": 0x7,
}
- },
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -71,19 +71,14 @@ async def test_one_pt2262(hass, rfxtrx):
async def test_pt2262_unconfigured(hass, rfxtrx):
"""Test with discovery for PT2262."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0913000022670e013970": {},
- "09130000226707013970": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={"0913000022670e013970": {}, "09130000226707013970": {}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -109,11 +104,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}},
- )
+ entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == state
@@ -121,20 +117,18 @@ async def test_state_restore(hass, rfxtrx, state, event):
async def test_several(hass, rfxtrx):
"""Test with 3."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1100cd0213c7f230010f71": {},
- "0b1100100118cdea02010f70": {},
- "0b1100101118cdea02010f70": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0b1100cd0213c7f230010f71": {},
+ "0b1100100118cdea02010f70": {},
+ "0b1100101118cdea02010f70": {},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.ac_213c7f2_48")
@@ -181,16 +175,12 @@ async def test_off_delay_restore(hass, rfxtrx):
],
)
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {EVENT_AC_118CDEA_2_ON: {"off_delay": 5}},
- }
- },
- )
+ entry_data = create_rfx_test_cfg(devices={EVENT_AC_118CDEA_2_ON: {"off_delay": 5}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -201,16 +191,14 @@ async def test_off_delay_restore(hass, rfxtrx):
async def test_off_delay(hass, rfxtrx, timestep):
"""Test with discovery."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {"0b1100100118cdea02010f70": {"off_delay": 5}},
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100100118cdea02010f70": {"off_delay": 5}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -295,27 +283,25 @@ async def test_light(hass, rfxtrx_automatic):
async def test_pt2262_duplicate_id(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0913000022670e013970": {
- "data_bits": 4,
- "command_on": 0xE,
- "command_off": 0x7,
- },
- "09130000226707013970": {
- "data_bits": 4,
- "command_on": 0xE,
- "command_off": 0x7,
- },
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0913000022670e013970": {
+ "data_bits": 4,
+ "command_on": 0xE,
+ "command_off": 0x7,
+ },
+ "09130000226707013970": {
+ "data_bits": 4,
+ "command_on": 0xE,
+ "command_off": 0x7,
+ },
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py
index 53f3b317d53533..6ba045d60a6d50 100644
--- a/tests/components/rfxtrx/test_config_flow.py
+++ b/tests/components/rfxtrx/test_config_flow.py
@@ -1,20 +1,307 @@
"""Test the Tado config flow."""
-from homeassistant import config_entries, setup
-from homeassistant.components.rfxtrx import DOMAIN
+import os
+import serial.tools.list_ports
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.rfxtrx import DOMAIN, config_flow
+from homeassistant.helpers.device_registry import (
+ async_entries_for_config_entry,
+ async_get_registry as async_get_device_registry,
+)
+from homeassistant.helpers.entity_registry import (
+ async_get_registry as async_get_entity_registry,
+)
+
+from tests.async_mock import MagicMock, patch, sentinel
from tests.common import MockConfigEntry
-async def test_import(hass):
- """Test we can import."""
- await setup.async_setup_component(hass, "persistent_notification", {})
+def serial_connect(self):
+ """Mock a serial connection."""
+ self.serial = True
+
+
+def serial_connect_fail(self):
+ """Mock a failed serial connection."""
+ self.serial = None
+
+def com_port():
+ """Mock of a serial port."""
+ port = serial.tools.list_ports_common.ListPortInfo()
+ port.serial_number = "1234"
+ port.manufacturer = "Virtual serial port"
+ port.device = "/dev/ttyUSB1234"
+ port.description = "Some serial port"
+
+ return port
+
+
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+ return_value=None,
+)
+async def test_setup_network(connect_mock, hass):
+ """Test we can setup network."""
result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
- data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Network"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_network"
+ assert result["errors"] == {}
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"host": "10.10.0.1", "port": 1234}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "RFXTRX"
+ assert result["data"] == {
+ "host": "10.10.0.1",
+ "port": 1234,
+ "device": None,
+ "automatic_add": False,
+ "devices": {},
+ }
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+ serial_connect,
+)
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+ return_value=None,
+)
+async def test_setup_serial(com_mock, connect_mock, hass):
+ """Test we can setup serial."""
+ port = com_port()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Serial"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {}
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": port.device}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "RFXTRX"
+ assert result["data"] == {
+ "host": None,
+ "port": None,
+ "device": port.device,
+ "automatic_add": False,
+ "devices": {},
+ }
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+ serial_connect,
+)
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+ return_value=None,
+)
+async def test_setup_serial_manual(com_mock, connect_mock, hass):
+ """Test we can setup serial with manual entry."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Serial"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": "Enter Manually"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial_manual_path"
+ assert result["errors"] == {}
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": "/dev/ttyUSB0"}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "RFXTRX"
+ assert result["data"] == {
+ "host": None,
+ "port": None,
+ "device": "/dev/ttyUSB0",
+ "automatic_add": False,
+ "devices": {},
+ }
+
+
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+ side_effect=OSError,
+)
+async def test_setup_network_fail(connect_mock, hass):
+ """Test we can setup network."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Network"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_network"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"host": "10.10.0.1", "port": 1234}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_network"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+ side_effect=serial.serialutil.SerialException,
+)
+async def test_setup_serial_fail(com_mock, connect_mock, hass):
+ """Test setup serial failed connection."""
+ port = com_port()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Serial"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": port.device}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+ serial_connect_fail,
+)
+async def test_setup_serial_manual_fail(com_mock, hass):
+ """Test setup serial failed connection."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"type": "Serial"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": "Enter Manually"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial_manual_path"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"device": "/dev/ttyUSB0"}
)
+ assert result["type"] == "form"
+ assert result["step_id"] == "setup_serial_manual_path"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+ serial_connect,
+)
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+ return_value=None,
+)
+async def test_import_serial(connect_mock, hass):
+ """Test we can import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+ )
+
assert result["type"] == "create_entry"
assert result["title"] == "RFXTRX"
assert result["data"] == {
@@ -25,13 +312,63 @@ async def test_import(hass):
}
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+ return_value=None,
+)
+async def test_import_network(connect_mock, hass):
+ """Test we can import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"host": "localhost", "port": 1234, "device": None, "debug": False},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "RFXTRX"
+ assert result["data"] == {
+ "host": "localhost",
+ "port": 1234,
+ "device": None,
+ "debug": False,
+ }
+
+
+@patch(
+ "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+ side_effect=OSError,
+)
+async def test_import_network_connection_fail(connect_mock, hass):
+ """Test we can import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"host": "localhost", "port": 1234, "device": None, "debug": False},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "cannot_connect"
+
+
async def test_import_update(hass):
"""Test we can import."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN,
- data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "debug": False,
+ "devices": {},
+ },
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
@@ -39,9 +376,775 @@ async def test_import_update(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={"host": None, "port": None, "device": "/dev/tty123", "debug": True},
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "debug": True,
+ "devices": {},
+ },
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
- assert entry.data["debug"]
+
+
+async def test_import_migrate(hass):
+ """Test we can import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "debug": True,
+ "automatic_add": True,
+ "devices": {},
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+ assert entry.data["devices"] == {}
+
+
+async def test_options_global(hass):
+ """Test if we can set global options."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {},
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"automatic_add": True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert entry.data["automatic_add"]
+
+
+async def test_options_add_device(hass):
+ """Test we can add a device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {},
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ # Try with invalid event code
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"automatic_add": True, "event_code": "1234"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+ assert result["errors"]
+ assert result["errors"]["event_code"] == "invalid_event_code"
+
+ # Try with valid event code
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": True,
+ "event_code": "0b1100cd0213c7f230010f71",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"fire_event": True, "signal_repetitions": 5}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert entry.data["automatic_add"]
+
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"]
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5
+ assert "delay_off" not in entry.data["devices"]["0b1100cd0213c7f230010f71"]
+
+ state = hass.states.get("binary_sensor.ac_213c7f2_48")
+ assert state
+ assert state.state == "off"
+ assert state.attributes.get("friendly_name") == "AC 213c7f2:48"
+
+
+async def test_options_add_duplicate_device(hass):
+ """Test we can add a device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "debug": False,
+ "automatic_add": False,
+ "devices": {"0b1100cd0213c7f230010f71": {"signal_repetitions": 1}},
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": True,
+ "event_code": "0b1100cd0213c7f230010f71",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+ assert result["errors"]
+ assert result["errors"]["event_code"] == "already_configured_device"
+
+
+async def test_options_add_remove_device(hass):
+ """Test we can add a device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {},
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": True,
+ "event_code": "0b1100cd0213c7f230010f71",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"fire_event": True, "signal_repetitions": 5, "off_delay": "4"},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert entry.data["automatic_add"]
+
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"]
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5
+ assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["off_delay"] == 4
+
+ state = hass.states.get("binary_sensor.ac_213c7f2_48")
+ assert state
+ assert state.state == "off"
+ assert state.attributes.get("friendly_name") == "AC 213c7f2:48"
+
+ device_registry = await async_get_device_registry(hass)
+ device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+ assert device_entries[0].id
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": False,
+ "remove_device": [device_entries[0].id],
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert not entry.data["automatic_add"]
+
+ assert "0b1100cd0213c7f230010f71" not in entry.data["devices"]
+
+ state = hass.states.get("binary_sensor.ac_213c7f2_48")
+ assert not state
+
+
+async def test_options_replace_sensor_device(hass):
+ """Test we can replace a sensor device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {
+ "0a520101f00400e22d0189": {"device_id": ["52", "1", "f0:04"]},
+ "0a520105230400c3260279": {"device_id": ["52", "1", "23:04"]},
+ },
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status"
+ )
+ assert state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature"
+ )
+ assert state
+
+ device_registry = await async_get_device_registry(hass)
+ device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+ old_device = next(
+ (
+ elem.id
+ for elem in device_entries
+ if next(iter(elem.identifiers))[1:] == ("52", "1", "f0:04")
+ ),
+ None,
+ )
+ new_device = next(
+ (
+ elem.id
+ for elem in device_entries
+ if next(iter(elem.identifiers))[1:] == ("52", "1", "23:04")
+ ),
+ None,
+ )
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": False,
+ "device": old_device,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "replace_device": new_device,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ entity_registry = await async_get_entity_registry(hass)
+
+ entry = entity_registry.async_get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric"
+ )
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity"
+ )
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status"
+ )
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric"
+ )
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature"
+ )
+ assert entry
+ assert entry.device_id == new_device
+
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric"
+ )
+ assert not state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric"
+ )
+ assert not state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity"
+ )
+ assert not state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status"
+ )
+ assert not state
+ state = hass.states.get(
+ "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature"
+ )
+ assert not state
+
+
+async def test_options_replace_control_device(hass):
+ """Test we can replace a control device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {
+ "0b1100100118cdea02010f70": {
+ "device_id": ["11", "0", "118cdea:2"],
+ "signal_repetitions": 1,
+ },
+ "0b1100101118cdea02010f70": {
+ "device_id": ["11", "0", "1118cdea:2"],
+ "signal_repetitions": 1,
+ },
+ },
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.ac_118cdea_2")
+ assert state
+ state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric")
+ assert state
+ state = hass.states.get("switch.ac_118cdea_2")
+ assert state
+ state = hass.states.get("binary_sensor.ac_1118cdea_2")
+ assert state
+ state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric")
+ assert state
+ state = hass.states.get("switch.ac_1118cdea_2")
+ assert state
+
+ device_registry = await async_get_device_registry(hass)
+ device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+ old_device = next(
+ (
+ elem.id
+ for elem in device_entries
+ if next(iter(elem.identifiers))[1:] == ("11", "0", "118cdea:2")
+ ),
+ None,
+ )
+ new_device = next(
+ (
+ elem.id
+ for elem in device_entries
+ if next(iter(elem.identifiers))[1:] == ("11", "0", "1118cdea:2")
+ ),
+ None,
+ )
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": False,
+ "device": old_device,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "replace_device": new_device,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ entity_registry = await async_get_entity_registry(hass)
+
+ entry = entity_registry.async_get("binary_sensor.ac_118cdea_2")
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric")
+ assert entry
+ assert entry.device_id == new_device
+ entry = entity_registry.async_get("switch.ac_118cdea_2")
+ assert entry
+ assert entry.device_id == new_device
+
+ state = hass.states.get("binary_sensor.ac_1118cdea_2")
+ assert not state
+ state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric")
+ assert not state
+ state = hass.states.get("switch.ac_1118cdea_2")
+ assert not state
+
+
+async def test_options_remove_multiple_devices(hass):
+ """Test we can add a device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {
+ "0b1100cd0213c7f230010f71": {"device_id": ["11", "0", "213c7f2:48"]},
+ "0b1100100118cdea02010f70": {"device_id": ["11", "0", "118cdea:2"]},
+ "0b1100101118cdea02010f70": {"device_id": ["11", "0", "1118cdea:2"]},
+ },
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.ac_213c7f2_48")
+ assert state
+ state = hass.states.get("binary_sensor.ac_118cdea_2")
+ assert state
+ state = hass.states.get("binary_sensor.ac_1118cdea_2")
+ assert state
+
+ device_registry = await async_get_device_registry(hass)
+ device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+ assert len(device_entries) == 3
+
+ def match_device_id(entry):
+ device_id = next(iter(entry.identifiers))[1:]
+ if device_id == ("11", "0", "213c7f2:48"):
+ return True
+ if device_id == ("11", "0", "118cdea:2"):
+ return True
+ return False
+
+ remove_devices = [elem.id for elem in device_entries if match_device_id(elem)]
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": False,
+ "remove_device": remove_devices,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.ac_213c7f2_48")
+ assert not state
+ state = hass.states.get("binary_sensor.ac_118cdea_2")
+ assert not state
+ state = hass.states.get("binary_sensor.ac_1118cdea_2")
+ assert state
+
+
+async def test_options_add_and_configure_device(hass):
+ """Test we can add a device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": None,
+ "port": None,
+ "device": "/dev/tty123",
+ "automatic_add": False,
+ "devices": {},
+ },
+ unique_id=DOMAIN,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": True,
+ "event_code": "0913000022670e013970",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "fire_event": False,
+ "signal_repetitions": 5,
+ "data_bits": 4,
+ "off_delay": "abcdef",
+ "command_on": "xyz",
+ "command_off": "xyz",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+ assert result["errors"]
+ assert result["errors"]["off_delay"] == "invalid_input_off_delay"
+ assert result["errors"]["command_on"] == "invalid_input_2262_on"
+ assert result["errors"]["command_off"] == "invalid_input_2262_off"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "fire_event": False,
+ "signal_repetitions": 5,
+ "data_bits": 4,
+ "command_on": "0xE",
+ "command_off": "0x7",
+ "off_delay": "9",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert entry.data["automatic_add"]
+
+ assert entry.data["devices"]["0913000022670e013970"]
+ assert not entry.data["devices"]["0913000022670e013970"]["fire_event"]
+ assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5
+ assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9
+
+ state = hass.states.get("binary_sensor.pt2262_22670e")
+ assert state
+ assert state.state == "off"
+ assert state.attributes.get("friendly_name") == "PT2262 22670e"
+
+ device_registry = await async_get_device_registry(hass)
+ device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+ assert device_entries[0].id
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "prompt_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "automatic_add": False,
+ "device": device_entries[0].id,
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "set_device_options"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ "fire_event": True,
+ "signal_repetitions": 5,
+ "data_bits": 4,
+ "command_on": "0xE",
+ "command_off": "0x7",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert entry.data["devices"]["0913000022670e013970"]
+ assert entry.data["devices"]["0913000022670e013970"]["fire_event"]
+ assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5
+ assert "delay_off" not in entry.data["devices"]["0913000022670e013970"]
+
+
+def test_get_serial_by_id_no_dir():
+ """Test serial by id conversion if there's no /dev/serial/by-id."""
+ p1 = patch("os.path.isdir", MagicMock(return_value=False))
+ p2 = patch("os.scandir")
+ with p1 as is_dir_mock, p2 as scan_mock:
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.path
+ assert is_dir_mock.call_count == 1
+ assert scan_mock.call_count == 0
+
+
+def test_get_serial_by_id():
+ """Test serial by id conversion."""
+ p1 = patch("os.path.isdir", MagicMock(return_value=True))
+ p2 = patch("os.scandir")
+
+ def _realpath(path):
+ if path is sentinel.matched_link:
+ return sentinel.path
+ return sentinel.serial_link_path
+
+ p3 = patch("os.path.realpath", side_effect=_realpath)
+ with p1 as is_dir_mock, p2 as scan_mock, p3:
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.path
+ assert is_dir_mock.call_count == 1
+ assert scan_mock.call_count == 1
+
+ entry1 = MagicMock(spec_set=os.DirEntry)
+ entry1.is_symlink.return_value = True
+ entry1.path = sentinel.some_path
+
+ entry2 = MagicMock(spec_set=os.DirEntry)
+ entry2.is_symlink.return_value = False
+ entry2.path = sentinel.other_path
+
+ entry3 = MagicMock(spec_set=os.DirEntry)
+ entry3.is_symlink.return_value = True
+ entry3.path = sentinel.matched_link
+
+ scan_mock.return_value = [entry1, entry2, entry3]
+ res = config_flow.get_serial_by_id(sentinel.path)
+ assert res is sentinel.matched_link
+ assert is_dir_mock.call_count == 2
+ assert scan_mock.call_count == 2
diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py
index 05ce26ebc109e6..b3e5ce224c6e13 100644
--- a/tests/components/rfxtrx/test_cover.py
+++ b/tests/components/rfxtrx/test_cover.py
@@ -3,19 +3,23 @@
import pytest
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import State
-from homeassistant.setup import async_setup_component
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
async def test_one_cover(hass, rfxtrx):
"""Test with 1 cover."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
@@ -57,11 +61,14 @@ async def test_state_restore(hass, rfxtrx, state):
mock_restore_cache(hass, [State(entity_id, state)])
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == state
@@ -69,20 +76,18 @@ async def test_state_restore(hass, rfxtrx, state):
async def test_several_covers(hass, rfxtrx):
"""Test with 3 covers."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1400cd0213c7f20d010f51": {},
- "0A1400ADF394AB010D0060": {},
- "09190000009ba8010100": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1},
+ "0A1400ADF394AB010D0060": {"signal_repetitions": 1},
+ "09190000009ba8010100": {"signal_repetitions": 1},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
@@ -118,19 +123,17 @@ async def test_discover_covers(hass, rfxtrx_automatic):
async def test_duplicate_cover(hass, rfxtrx):
"""Test with 2 duplicate covers."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1400cd0213c7f20d010f51": {},
- "0b1400cd0213c7f20d010f50": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1},
+ "0b1400cd0213c7f20d010f50": {"signal_repetitions": 1},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py
index abe7c3c0441773..037b08b7cc6420 100644
--- a/tests/components/rfxtrx/test_init.py
+++ b/tests/components/rfxtrx/test_init.py
@@ -1,10 +1,13 @@
"""The tests for the Rfxtrx component."""
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
from tests.async_mock import call
+from tests.common import MockConfigEntry
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
async def test_valid_config(hass):
@@ -55,21 +58,19 @@ async def test_invalid_config(hass):
async def test_fire_event(hass, rfxtrx):
"""Test fire event."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "/dev/serial/by-id/usb"
- + "-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0",
- "automatic_add": True,
- "devices": {
- "0b1100cd0213c7f210010f51": {"fire_event": True},
- "0716000100900970": {"fire_event": True},
- },
- }
+ entry_data = create_rfx_test_cfg(
+ device="/dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0",
+ automatic_add=True,
+ devices={
+ "0b1100cd0213c7f210010f51": {"fire_event": True},
+ "0716000100900970": {"fire_event": True},
},
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -101,16 +102,19 @@ def record_event(event):
"type_string": "Byron SX",
"id_string": "00:90",
"data": "0716000100900970",
- "values": {"Sound": 9, "Battery numeric": 0, "Rssi numeric": 7},
+ "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9},
},
]
async def test_send(hass, rfxtrx):
"""Test configuration."""
- assert await async_setup_component(
- hass, "rfxtrx", {"rfxtrx": {"device": "/dev/null"}}
- )
+ entry_data = create_rfx_test_cfg(device="/dev/null", devices={})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py
index f6f056fa16a0e5..78151c5fa9c262 100644
--- a/tests/components/rfxtrx/test_light.py
+++ b/tests/components/rfxtrx/test_light.py
@@ -4,19 +4,23 @@
import pytest
from homeassistant.components.light import ATTR_BRIGHTNESS
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import State
-from homeassistant.setup import async_setup_component
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
async def test_one_light(hass, rfxtrx):
"""Test with 1 light."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("light.ac_213c7f2_16")
@@ -95,11 +99,14 @@ async def test_state_restore(hass, rfxtrx, state, brightness):
hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})]
)
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == state
@@ -108,20 +115,18 @@ async def test_state_restore(hass, rfxtrx, state, brightness):
async def test_several_lights(hass, rfxtrx):
"""Test with 3 lights."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1100cd0213c7f230020f71": {},
- "0b1100100118cdea02020f70": {},
- "0b1100101118cdea02050f70": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0b1100cd0213c7f230020f71": {"signal_repetitions": 1},
+ "0b1100100118cdea02020f70": {"signal_repetitions": 1},
+ "0b1100101118cdea02050f70": {"signal_repetitions": 1},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -160,18 +165,14 @@ async def test_several_lights(hass, rfxtrx):
@pytest.mark.parametrize("repetitions", [1, 3])
async def test_repetitions(hass, rfxtrx, repetitions):
"""Test signal repetitions."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions}
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py
index 18239550a852d7..77f9960de49e14 100644
--- a/tests/components/rfxtrx/test_sensor.py
+++ b/tests/components/rfxtrx/test_sensor.py
@@ -1,19 +1,28 @@
"""The tests for the Rfxtrx sensor platform."""
import pytest
+from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import ATTR_EVENT
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+ TEMP_CELSIUS,
+)
from homeassistant.core import State
-from homeassistant.setup import async_setup_component
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
async def test_default_config(hass, rfxtrx):
"""Test with 0 sensor."""
- await async_setup_component(
- hass, "sensor", {"sensor": {"platform": "rfxtrx", "devices": {}}}
- )
+ entry_data = create_rfx_test_cfg(devices={})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
@@ -21,11 +30,12 @@ async def test_default_config(hass, rfxtrx):
async def test_one_sensor(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}},
- )
+ entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02_temperature")
@@ -35,7 +45,7 @@ async def test_one_sensor(hass, rfxtrx):
state.attributes.get("friendly_name")
== "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature"
)
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
@pytest.mark.parametrize(
@@ -49,11 +59,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0a520801070100b81b0279": {}}}},
- )
+ entry_data = create_rfx_test_cfg(devices={"0a520801070100b81b0279": {}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == state
@@ -61,11 +72,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
async def test_one_sensor_no_datatype(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}},
- )
+ entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
base_id = "sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02"
@@ -75,48 +87,49 @@ async def test_one_sensor_no_datatype(hass, rfxtrx):
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == f"{base_name} Temperature"
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get(f"{base_id}_humidity")
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == f"{base_name} Humidity"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
state = hass.states.get(f"{base_id}_humidity_status")
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == f"{base_name} Humidity status"
- assert state.attributes.get("unit_of_measurement") == ""
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
state = hass.states.get(f"{base_id}_rssi_numeric")
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
state = hass.states.get(f"{base_id}_battery_numeric")
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
async def test_several_sensors(hass, rfxtrx):
"""Test with 3 sensors."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0a52080705020095220269": {},
- "0a520802060100ff0e0269": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0a52080705020095220269": {},
+ "0a520802060100ff0e0269": {},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -127,7 +140,7 @@ async def test_several_sensors(hass, rfxtrx):
state.attributes.get("friendly_name")
== "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature"
)
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_temperature")
assert state
@@ -136,7 +149,7 @@ async def test_several_sensors(hass, rfxtrx):
state.attributes.get("friendly_name")
== "WT260,WT260H,WT440H,WT450,WT450H 06:01 Temperature"
)
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_humidity")
assert state
@@ -145,7 +158,7 @@ async def test_several_sensors(hass, rfxtrx):
state.attributes.get("friendly_name")
== "WT260,WT260H,WT440H,WT450,WT450H 06:01 Humidity"
)
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
async def test_discover_sensor(hass, rfxtrx_automatic):
@@ -159,27 +172,30 @@ async def test_discover_sensor(hass, rfxtrx_automatic):
state = hass.states.get(f"{base_id}_humidity")
assert state
assert state.state == "27"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
state = hass.states.get(f"{base_id}_humidity_status")
assert state
assert state.state == "normal"
- assert state.attributes.get("unit_of_measurement") == ""
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
state = hass.states.get(f"{base_id}_rssi_numeric")
assert state
assert state.state == "-64"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
state = hass.states.get(f"{base_id}_temperature")
assert state
assert state.state == "18.4"
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get(f"{base_id}_battery_numeric")
assert state
- assert state.state == "90"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.state == "100"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
# 2
await rfxtrx.signal("0a52080405020095240279")
@@ -188,27 +204,30 @@ async def test_discover_sensor(hass, rfxtrx_automatic):
assert state
assert state.state == "36"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
state = hass.states.get(f"{base_id}_humidity_status")
assert state
assert state.state == "normal"
- assert state.attributes.get("unit_of_measurement") == ""
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
state = hass.states.get(f"{base_id}_rssi_numeric")
assert state
assert state.state == "-64"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
state = hass.states.get(f"{base_id}_temperature")
assert state
assert state.state == "14.9"
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get(f"{base_id}_battery_numeric")
assert state
- assert state.state == "90"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.state == "100"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
# 1 Update
await rfxtrx.signal("0a52085e070100b31b0279")
@@ -217,46 +236,47 @@ async def test_discover_sensor(hass, rfxtrx_automatic):
state = hass.states.get(f"{base_id}_humidity")
assert state
assert state.state == "27"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
state = hass.states.get(f"{base_id}_humidity_status")
assert state
assert state.state == "normal"
- assert state.attributes.get("unit_of_measurement") == ""
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
state = hass.states.get(f"{base_id}_rssi_numeric")
assert state
assert state.state == "-64"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
state = hass.states.get(f"{base_id}_temperature")
assert state
assert state.state == "17.9"
- assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
state = hass.states.get(f"{base_id}_battery_numeric")
assert state
- assert state.state == "90"
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.state == "100"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert len(hass.states.async_all()) == 10
async def test_update_of_sensors(hass, rfxtrx):
"""Test with 3 sensors."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0a52080705020095220269": {},
- "0a520802060100ff0e0269": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0a52080705020095220269": {},
+ "0a520802060100ff0e0269": {},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -290,23 +310,21 @@ async def test_update_of_sensors(hass, rfxtrx):
async def test_rssi_sensor(hass, rfxtrx):
"""Test with 1 sensor."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0913000022670e013b70": {
- "data_bits": 4,
- "command_on": 0xE,
- "command_off": 0x7,
- },
- "0b1100cd0213c7f230010f71": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0913000022670e013b70": {
+ "data_bits": 4,
+ "command_on": 0xE,
+ "command_off": 0x7,
+ },
+ "0b1100cd0213c7f230010f71": {},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.async_start()
@@ -314,13 +332,19 @@ async def test_rssi_sensor(hass, rfxtrx):
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric")
assert state
assert state.state == "unknown"
assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric"
- assert state.attributes.get("unit_of_measurement") == "dBm"
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
await rfxtrx.signal("0913000022670e013b70")
await rfxtrx.signal("0b1100cd0213c7f230010f71")
diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py
index 1fed6a65562ebf..ee4fd265fc9574 100644
--- a/tests/components/rfxtrx/test_switch.py
+++ b/tests/components/rfxtrx/test_switch.py
@@ -5,9 +5,9 @@
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import State
-from homeassistant.setup import async_setup_component
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113"
EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114"
@@ -15,11 +15,14 @@
async def test_one_switch(hass, rfxtrx):
"""Test with 1 switch."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("switch.ac_213c7f2_16")
@@ -55,11 +58,14 @@ async def test_state_restore(hass, rfxtrx, state):
mock_restore_cache(hass, [State(entity_id, state)])
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}},
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == state
@@ -67,20 +73,18 @@ async def test_state_restore(hass, rfxtrx, state):
async def test_several_switches(hass, rfxtrx):
"""Test with 3 switches."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1100cd0213c7f230010f71": {},
- "0b1100100118cdea02010f70": {},
- "0b1100101118cdea02010f70": {},
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={
+ "0b1100cd0213c7f230010f71": {"signal_repetitions": 1},
+ "0b1100100118cdea02010f70": {"signal_repetitions": 1},
+ "0b1100101118cdea02010f70": {"signal_repetitions": 1},
+ }
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("switch.ac_213c7f2_48")
@@ -102,18 +106,14 @@ async def test_several_switches(hass, rfxtrx):
@pytest.mark.parametrize("repetitions", [1, 3])
async def test_repetitions(hass, rfxtrx, repetitions):
"""Test signal repetitions."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {
- "0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions}
- },
- }
- },
+ entry_data = create_rfx_test_cfg(
+ devices={"0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions}}
)
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
@@ -156,16 +156,12 @@ async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic):
async def test_unknown_event_code(hass, rfxtrx):
"""Test with 3 switches."""
- assert await async_setup_component(
- hass,
- "rfxtrx",
- {
- "rfxtrx": {
- "device": "abcd",
- "devices": {"1234567890": {}},
- }
- },
- )
+ entry_data = create_rfx_test_cfg(devices={"1234567890": {"signal_repetitions": 1}})
+ mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
conf_entries = hass.config_entries.async_entries(DOMAIN)
diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py
index 4ad056150dd6a2..c7a87ae880a172 100644
--- a/tests/components/ring/test_init.py
+++ b/tests/components/ring/test_init.py
@@ -1,13 +1,10 @@
"""The tests for the Ring component."""
-from asyncio import run_coroutine_threadsafe
from datetime import timedelta
-import unittest
-
-import requests_mock
import homeassistant.components.ring as ring
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant, load_fixture
+from tests.common import load_fixture
ATTRIBUTION = "Data provided by Ring.com"
@@ -16,43 +13,28 @@
}
-class TestRing(unittest.TestCase):
- """Tests the Ring component."""
-
- def setUp(self):
- """Initialize values for this test case class."""
- self.hass = get_test_home_assistant()
- self.config = VALID_CONFIG
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- @requests_mock.Mocker()
- def test_setup(self, mock):
- """Test the setup."""
- mock.post(
- "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json")
- )
- mock.post(
- "https://api.ring.com/clients_api/session",
- text=load_fixture("ring_session.json"),
- )
- mock.get(
- "https://api.ring.com/clients_api/ring_devices",
- text=load_fixture("ring_devices.json"),
- )
- mock.get(
- "https://api.ring.com/clients_api/chimes/999999/health",
- text=load_fixture("ring_chime_health_attrs.json"),
- )
- mock.get(
- "https://api.ring.com/clients_api/doorbots/987652/health",
- text=load_fixture("ring_doorboot_health_attrs.json"),
- )
- response = run_coroutine_threadsafe(
- ring.async_setup(self.hass, self.config), self.hass.loop
- ).result()
-
- assert response
+async def test_setup(hass, requests_mock):
+ """Test the setup."""
+ await async_setup_component(hass, ring.DOMAIN, {})
+
+ requests_mock.post(
+ "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json")
+ )
+ requests_mock.post(
+ "https://api.ring.com/clients_api/session",
+ text=load_fixture("ring_session.json"),
+ )
+ requests_mock.get(
+ "https://api.ring.com/clients_api/ring_devices",
+ text=load_fixture("ring_devices.json"),
+ )
+ requests_mock.get(
+ "https://api.ring.com/clients_api/chimes/999999/health",
+ text=load_fixture("ring_chime_health_attrs.json"),
+ )
+ requests_mock.get(
+ "https://api.ring.com/clients_api/doorbots/987652/health",
+ text=load_fixture("ring_doorboot_health_attrs.json"),
+ )
+
+ assert await ring.async_setup(hass, VALID_CONFIG)
diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py
index e9d5091d664de4..b4ce1811c915bb 100644
--- a/tests/components/roku/test_media_player.py
+++ b/tests/components/roku/test_media_player.py
@@ -529,6 +529,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
+ assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 11
@@ -573,6 +574,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]["title"] == "Channels"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS
+ assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 2
diff --git a/tests/components/rpi_power/__init__.py b/tests/components/rpi_power/__init__.py
new file mode 100644
index 00000000000000..25705bd854f851
--- /dev/null
+++ b/tests/components/rpi_power/__init__.py
@@ -0,0 +1 @@
+"""Tests for rpi_power."""
diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py
new file mode 100644
index 00000000000000..873f654aa3bca6
--- /dev/null
+++ b/tests/components/rpi_power/test_binary_sensor.py
@@ -0,0 +1,73 @@
+"""Tests for rpi_power binary sensor."""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.rpi_power.binary_sensor import (
+ DESCRIPTION_NORMALIZED,
+ DESCRIPTION_UNDER_VOLTAGE,
+)
+from homeassistant.components.rpi_power.const import DOMAIN
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from tests.async_mock import MagicMock
+from tests.common import MockConfigEntry, async_fire_time_changed, patch
+
+ENTITY_ID = "binary_sensor.rpi_power_status"
+
+MODULE = "homeassistant.components.rpi_power.binary_sensor.new_under_voltage"
+
+
+async def _async_setup_component(hass, detected):
+ mocked_under_voltage = MagicMock()
+ type(mocked_under_voltage).get = MagicMock(return_value=detected)
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+ with patch(MODULE, return_value=mocked_under_voltage):
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+ await hass.async_block_till_done()
+ return mocked_under_voltage
+
+
+async def test_new(hass, caplog):
+ """Test new entry."""
+ await _async_setup_component(hass, False)
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == STATE_OFF
+ assert not any(x.levelno == logging.WARNING for x in caplog.records)
+
+
+async def test_new_detected(hass, caplog):
+ """Test new entry with under voltage detected."""
+ mocked_under_voltage = await _async_setup_component(hass, True)
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == STATE_ON
+ assert (
+ len(
+ [
+ x
+ for x in caplog.records
+ if x.levelno == logging.WARNING
+ and x.message == DESCRIPTION_UNDER_VOLTAGE
+ ]
+ )
+ == 1
+ )
+
+ # back to normal
+ type(mocked_under_voltage).get = MagicMock(return_value=False)
+ future = dt_util.utcnow() + timedelta(minutes=1)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+ state = hass.states.get(ENTITY_ID)
+ assert (
+ len(
+ [
+ x
+ for x in caplog.records
+ if x.levelno == logging.INFO and x.message == DESCRIPTION_NORMALIZED
+ ]
+ )
+ == 1
+ )
diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py
new file mode 100644
index 00000000000000..090b6a6a793588
--- /dev/null
+++ b/tests/components/rpi_power/test_config_flow.py
@@ -0,0 +1,63 @@
+"""Tests for rpi_power config flow."""
+from homeassistant.components.rpi_power.const import DOMAIN
+from homeassistant.config_entries 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.async_mock import MagicMock
+from tests.common import patch
+
+MODULE = "homeassistant.components.rpi_power.config_flow.new_under_voltage"
+
+
+async def test_setup(hass: HomeAssistant) -> None:
+ """Test setting up manually."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert not result["errors"]
+
+ with patch(MODULE, return_value=MagicMock()):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_not_supported(hass: HomeAssistant) -> None:
+ """Test setting up on not supported system."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+
+ with patch(MODULE, return_value=None):
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "no_devices_found"
+
+
+async def test_onboarding(hass: HomeAssistant) -> None:
+ """Test setting up via onboarding."""
+ with patch(MODULE, return_value=MagicMock()):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "onboarding"},
+ )
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_onboarding_not_supported(hass: HomeAssistant) -> None:
+ """Test setting up via onboarding with unsupported system."""
+ with patch(MODULE, return_value=None):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "onboarding"},
+ )
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "no_devices_found"
diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py
index 0e83255c6e372d..f8a362e984218f 100644
--- a/tests/components/samsungtv/test_config_flow.py
+++ b/tests/components/samsungtv/test_config_flow.py
@@ -211,7 +211,7 @@ async def test_user_not_successful(hass):
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "abort"
- assert result["reason"] == "not_successful"
+ assert result["reason"] == "cannot_connect"
async def test_user_not_successful_2(hass):
@@ -229,7 +229,7 @@ async def test_user_not_successful_2(hass):
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "abort"
- assert result["reason"] == "not_successful"
+ assert result["reason"] == "cannot_connect"
async def test_user_already_configured(hass, remote):
@@ -389,7 +389,7 @@ async def test_ssdp_not_successful(hass):
result["flow_id"], user_input="whatever"
)
assert result["type"] == "abort"
- assert result["reason"] == "not_successful"
+ assert result["reason"] == "cannot_connect"
async def test_ssdp_not_successful_2(hass):
@@ -416,7 +416,7 @@ async def test_ssdp_not_successful_2(hass):
result["flow_id"], user_input="whatever"
)
assert result["type"] == "abort"
- assert result["reason"] == "not_successful"
+ assert result["reason"] == "cannot_connect"
async def test_ssdp_already_in_progress(hass, remote):
@@ -570,7 +570,7 @@ async def test_autodetect_none(hass, remote, remotews):
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "abort"
- assert result["reason"] == "not_successful"
+ assert result["reason"] == "cannot_connect"
assert remote.call_count == 1
assert remote.call_args_list == [
call(AUTODETECT_LEGACY),
diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py
index 8918099405a13e..943036e9c00ccc 100644
--- a/tests/components/season/test_sensor.py
+++ b/tests/components/season/test_sensor.py
@@ -1,12 +1,20 @@
"""The tests for the Season sensor platform."""
-# pylint: disable=protected-access
from datetime import datetime
-import unittest
-import homeassistant.components.season.sensor as season
-from homeassistant.setup import setup_component
+import pytest
-from tests.common import get_test_home_assistant
+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 tests.async_mock import patch
HEMISPHERE_NORTHERN = {
"homeassistant": {"latitude": "48.864716", "longitude": "2.349014"},
@@ -28,217 +36,90 @@
"sensor": {"platform": "season", "type": "meteorological"},
}
-
-# pylint: disable=invalid-name
-class TestSeason(unittest.TestCase):
- """Test the season platform."""
-
- DEVICE = None
- CONFIG_ASTRONOMICAL = {"type": "astronomical"}
- CONFIG_METEOROLOGICAL = {"type": "meteorological"}
-
- def add_entities(self, devices):
- """Mock add devices."""
- for device in devices:
- self.DEVICE = device
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- def test_season_should_be_summer_northern_astronomical(self):
- """Test that season should be summer."""
- # A known day in summer
- summer_day = datetime(2017, 9, 3, 0, 0)
- current_season = season.get_season(
- summer_day, season.NORTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_SUMMER == current_season
-
- def test_season_should_be_summer_northern_meteorological(self):
- """Test that season should be summer."""
- # A known day in summer
- summer_day = datetime(2017, 8, 13, 0, 0)
- current_season = season.get_season(
- summer_day, season.NORTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_SUMMER == current_season
-
- def test_season_should_be_autumn_northern_astronomical(self):
- """Test that season should be autumn."""
- # A known day in autumn
- autumn_day = datetime(2017, 9, 23, 0, 0)
- current_season = season.get_season(
- autumn_day, season.NORTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_AUTUMN == current_season
-
- def test_season_should_be_autumn_northern_meteorological(self):
- """Test that season should be autumn."""
- # A known day in autumn
- autumn_day = datetime(2017, 9, 3, 0, 0)
- current_season = season.get_season(
- autumn_day, season.NORTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_AUTUMN == current_season
-
- def test_season_should_be_winter_northern_astronomical(self):
- """Test that season should be winter."""
- # A known day in winter
- winter_day = datetime(2017, 12, 25, 0, 0)
- current_season = season.get_season(
- winter_day, season.NORTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_WINTER == current_season
-
- def test_season_should_be_winter_northern_meteorological(self):
- """Test that season should be winter."""
- # A known day in winter
- winter_day = datetime(2017, 12, 3, 0, 0)
- current_season = season.get_season(
- winter_day, season.NORTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_WINTER == current_season
-
- def test_season_should_be_spring_northern_astronomical(self):
- """Test that season should be spring."""
- # A known day in spring
- spring_day = datetime(2017, 4, 1, 0, 0)
- current_season = season.get_season(
- spring_day, season.NORTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_SPRING == current_season
-
- def test_season_should_be_spring_northern_meteorological(self):
- """Test that season should be spring."""
- # A known day in spring
- spring_day = datetime(2017, 3, 3, 0, 0)
- current_season = season.get_season(
- spring_day, season.NORTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_SPRING == current_season
-
- def test_season_should_be_winter_southern_astronomical(self):
- """Test that season should be winter."""
- # A known day in winter
- winter_day = datetime(2017, 9, 3, 0, 0)
- current_season = season.get_season(
- winter_day, season.SOUTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_WINTER == current_season
-
- def test_season_should_be_winter_southern_meteorological(self):
- """Test that season should be winter."""
- # A known day in winter
- winter_day = datetime(2017, 8, 13, 0, 0)
- current_season = season.get_season(
- winter_day, season.SOUTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_WINTER == current_season
-
- def test_season_should_be_spring_southern_astronomical(self):
- """Test that season should be spring."""
- # A known day in spring
- spring_day = datetime(2017, 9, 23, 0, 0)
- current_season = season.get_season(
- spring_day, season.SOUTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_SPRING == current_season
-
- def test_season_should_be_spring_southern_meteorological(self):
- """Test that season should be spring."""
- # A known day in spring
- spring_day = datetime(2017, 9, 3, 0, 0)
- current_season = season.get_season(
- spring_day, season.SOUTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_SPRING == current_season
-
- def test_season_should_be_summer_southern_astronomical(self):
- """Test that season should be summer."""
- # A known day in summer
- summer_day = datetime(2017, 12, 25, 0, 0)
- current_season = season.get_season(
- summer_day, season.SOUTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_SUMMER == current_season
-
- def test_season_should_be_summer_southern_meteorological(self):
- """Test that season should be summer."""
- # A known day in summer
- summer_day = datetime(2017, 12, 3, 0, 0)
- current_season = season.get_season(
- summer_day, season.SOUTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_SUMMER == current_season
-
- def test_season_should_be_autumn_southern_astronomical(self):
- """Test that season should be spring."""
- # A known day in spring
- autumn_day = datetime(2017, 4, 1, 0, 0)
- current_season = season.get_season(
- autumn_day, season.SOUTHERN, season.TYPE_ASTRONOMICAL
- )
- assert season.STATE_AUTUMN == current_season
-
- def test_season_should_be_autumn_southern_meteorological(self):
- """Test that season should be autumn."""
- # A known day in autumn
- autumn_day = datetime(2017, 3, 3, 0, 0)
- current_season = season.get_season(
- autumn_day, season.SOUTHERN, season.TYPE_METEOROLOGICAL
- )
- assert season.STATE_AUTUMN == current_season
-
- def test_on_equator_results_in_none(self):
- """Test that season should be unknown."""
- # A known day in summer if astronomical and northern
- summer_day = datetime(2017, 9, 3, 0, 0)
- current_season = season.get_season(
- summer_day, season.EQUATOR, season.TYPE_ASTRONOMICAL
- )
- assert current_season is None
-
- def test_setup_hemisphere_northern(self):
- """Test platform setup of northern hemisphere."""
- self.hass.config.latitude = HEMISPHERE_NORTHERN["homeassistant"]["latitude"]
- assert setup_component(self.hass, "sensor", HEMISPHERE_NORTHERN)
- self.hass.block_till_done()
- assert (
- self.hass.config.as_dict()["latitude"]
- == HEMISPHERE_NORTHERN["homeassistant"]["latitude"]
- )
- state = self.hass.states.get("sensor.season")
- assert state.attributes.get("friendly_name") == "Season"
-
- def test_setup_hemisphere_southern(self):
- """Test platform setup of southern hemisphere."""
- self.hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
- assert setup_component(self.hass, "sensor", HEMISPHERE_SOUTHERN)
- self.hass.block_till_done()
- assert (
- self.hass.config.as_dict()["latitude"]
- == HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
- )
- state = self.hass.states.get("sensor.season")
- assert state.attributes.get("friendly_name") == "Season"
-
- def test_setup_hemisphere_equator(self):
- """Test platform setup of equator."""
- self.hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
- assert setup_component(self.hass, "sensor", HEMISPHERE_EQUATOR)
- self.hass.block_till_done()
- assert (
- self.hass.config.as_dict()["latitude"]
- == HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
- )
- state = self.hass.states.get("sensor.season")
- assert state.attributes.get("friendly_name") == "Season"
-
- def test_setup_hemisphere_empty(self):
- """Test platform setup of missing latlong."""
- self.hass.config.latitude = None
- assert setup_component(self.hass, "sensor", HEMISPHERE_EMPTY)
- self.hass.block_till_done()
- assert self.hass.config.as_dict()["latitude"] is None
+NORTHERN_PARAMETERS = [
+ (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_SUMMER),
+ (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_SUMMER),
+ (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_AUTUMN),
+ (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_AUTUMN),
+ (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_WINTER),
+ (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_WINTER),
+ (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_SPRING),
+ (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_SPRING),
+]
+
+SOUTHERN_PARAMETERS = [
+ (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_SUMMER),
+ (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_SUMMER),
+ (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_AUTUMN),
+ (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_AUTUMN),
+ (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_WINTER),
+ (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_WINTER),
+ (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_SPRING),
+ (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_SPRING),
+]
+
+
+def idfn(val):
+ """Provide IDs for pytest parametrize."""
+ if isinstance(val, (datetime)):
+ return val.strftime("%Y%m%d")
+
+
+@pytest.mark.parametrize("type,day,expected", NORTHERN_PARAMETERS, ids=idfn)
+async def test_season_northern_hemisphere(hass, type, day, expected):
+ """Test that season should be summer."""
+ hass.config.latitude = HEMISPHERE_NORTHERN["homeassistant"]["latitude"]
+
+ 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)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.season")
+ assert state
+ assert state.state == expected
+
+
+@pytest.mark.parametrize("type,day,expected", SOUTHERN_PARAMETERS, ids=idfn)
+async def test_season_southern_hemisphere(hass, type, day, expected):
+ """Test that season should be summer."""
+ hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
+
+ 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)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.season")
+ assert state
+ assert state.state == expected
+
+
+async def test_season_equator(hass):
+ """Test that season should be unknown for equator."""
+ hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
+ day = datetime(2017, 9, 3, 0, 0)
+
+ with patch("homeassistant.components.season.sensor.utcnow", return_value=day):
+ assert await async_setup_component(hass, "sensor", HEMISPHERE_EQUATOR)
+ 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
diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py
index 330f4a66152eb4..798620e2af7c48 100644
--- a/tests/components/seventeentrack/test_sensor.py
+++ b/tests/components/seventeentrack/test_sensor.py
@@ -140,6 +140,8 @@ async def _goto_future(hass, future=None):
with patch("homeassistant.util.utcnow", return_value=future):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
async def test_full_valid_config(hass):
@@ -247,6 +249,8 @@ async def test_delivered_not_shown(hass):
hass.components.persistent_notification = MagicMock()
await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED)
+ await _goto_future(hass)
+
assert not hass.states.async_entity_ids()
hass.components.persistent_notification.create.assert_called()
diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py
index 93192e89df3ffb..03eb907d09b27d 100644
--- a/tests/components/shelly/test_config_flow.py
+++ b/tests/components/shelly/test_config_flow.py
@@ -2,6 +2,7 @@
import asyncio
import aiohttp
+import aioshelly
import pytest
from homeassistant import config_entries, setup
@@ -25,9 +26,11 @@ async def test_form(hass):
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
), patch(
"aioshelly.Device.create",
- return_value=Mock(
- shutdown=AsyncMock(),
- settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ new=AsyncMock(
+ return_value=Mock(
+ shutdown=AsyncMock(),
+ settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ )
),
), patch(
"homeassistant.components.shelly.async_setup", return_value=True
@@ -72,9 +75,11 @@ async def test_form_auth(hass):
with patch(
"aioshelly.Device.create",
- return_value=Mock(
- shutdown=AsyncMock(),
- settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ new=AsyncMock(
+ return_value=Mock(
+ shutdown=AsyncMock(),
+ settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ )
),
), patch(
"homeassistant.components.shelly.async_setup", return_value=True
@@ -109,10 +114,7 @@ async def test_form_errors_get_info(hass, error):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch(
- "aioshelly.get_info",
- side_effect=exc,
- ):
+ with patch("aioshelly.get_info", side_effect=exc):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "1.1.1.1"},
@@ -134,10 +136,7 @@ async def test_form_errors_test_connection(hass, error):
with patch(
"aioshelly.get_info", return_value={"mac": "test-mac", "auth": False}
- ), patch(
- "aioshelly.Device.create",
- side_effect=exc,
- ):
+ ), patch("aioshelly.Device.create", new=AsyncMock(side_effect=exc)):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "1.1.1.1"},
@@ -175,6 +174,22 @@ async def test_form_already_configured(hass):
assert entry.data["host"] == "1.1.1.1"
+async def test_form_firmware_unsupported(hass):
+ """Test we abort if device firmware is unsupported."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"host": "1.1.1.1"},
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "unsupported_firmware"
+
+
@pytest.mark.parametrize(
"error",
[
@@ -199,7 +214,7 @@ async def test_form_auth_errors_test_connection(hass, error):
with patch(
"aioshelly.Device.create",
- side_effect=exc,
+ new=AsyncMock(side_effect=exc),
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
@@ -227,9 +242,11 @@ async def test_zeroconf(hass):
with patch(
"aioshelly.Device.create",
- return_value=Mock(
- shutdown=AsyncMock(),
- settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ new=AsyncMock(
+ return_value=Mock(
+ shutdown=AsyncMock(),
+ settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ )
),
), patch(
"homeassistant.components.shelly.async_setup", return_value=True
@@ -274,7 +291,7 @@ async def test_zeroconf_confirm_error(hass, error):
with patch(
"aioshelly.Device.create",
- side_effect=exc,
+ new=AsyncMock(side_effect=exc),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -309,12 +326,22 @@ async def test_zeroconf_already_configured(hass):
assert entry.data["host"] == "1.1.1.1"
+async def test_zeroconf_firmware_unsupported(hass):
+ """Test we abort if device firmware is unsupported."""
+ with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data={"host": "1.1.1.1", "name": "shelly1pm-12345"},
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "unsupported_firmware"
+
+
async def test_zeroconf_cannot_connect(hass):
"""Test we get the form."""
- with patch(
- "aioshelly.get_info",
- side_effect=asyncio.TimeoutError,
- ):
+ with patch("aioshelly.get_info", side_effect=asyncio.TimeoutError):
result = await hass.config_entries.flow.async_init(
DOMAIN,
data={"host": "1.1.1.1", "name": "shelly1pm-12345"},
@@ -349,9 +376,11 @@ async def test_zeroconf_require_auth(hass):
with patch(
"aioshelly.Device.create",
- return_value=Mock(
- shutdown=AsyncMock(),
- settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ new=AsyncMock(
+ return_value=Mock(
+ shutdown=AsyncMock(),
+ settings={"name": "Test name", "device": {"mac": "test-mac"}},
+ )
),
), patch(
"homeassistant.components.shelly.async_setup", return_value=True
diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py
index 923392bbaf8ecb..a7d293a052ee69 100644
--- a/tests/components/sigfox/test_sensor.py
+++ b/tests/components/sigfox/test_sensor.py
@@ -1,6 +1,5 @@
"""Tests for the sigfox sensor."""
import re
-import unittest
import requests_mock
@@ -9,9 +8,7 @@
CONF_API_LOGIN,
CONF_API_PASSWORD,
)
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
+from homeassistant.setup import async_setup_component
TEST_API_LOGIN = "foo"
TEST_API_PASSWORD = "ebcd1234"
@@ -33,39 +30,32 @@
"""
-class TestSigfoxSensor(unittest.TestCase):
- """Test the sigfox platform."""
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
+async def test_invalid_credentials(hass):
+ """Test for invalid credentials."""
+ with requests_mock.Mocker() as mock_req:
+ url = re.compile(API_URL + "devicetypes")
+ mock_req.get(url, text="{}", status_code=401)
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids()) == 0
- def test_invalid_credentials(self):
- """Test for invalid credentials."""
- with requests_mock.Mocker() as mock_req:
- url = re.compile(API_URL + "devicetypes")
- mock_req.get(url, text="{}", status_code=401)
- assert setup_component(self.hass, "sensor", VALID_CONFIG)
- self.hass.block_till_done()
- assert len(self.hass.states.entity_ids()) == 0
- def test_valid_credentials(self):
- """Test for valid credentials."""
- with requests_mock.Mocker() as mock_req:
- url1 = re.compile(API_URL + "devicetypes")
- mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', status_code=200)
+async def test_valid_credentials(hass):
+ """Test for valid credentials."""
+ with requests_mock.Mocker() as mock_req:
+ url1 = re.compile(API_URL + "devicetypes")
+ mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', status_code=200)
- url2 = re.compile(API_URL + "devicetypes/fake_type/devices")
- mock_req.get(url2, text='{"data":[{"id":"fake_id"}]}')
+ url2 = re.compile(API_URL + "devicetypes/fake_type/devices")
+ mock_req.get(url2, text='{"data":[{"id":"fake_id"}]}')
- url3 = re.compile(API_URL + "devices/fake_id/messages*")
- mock_req.get(url3, text=VALID_MESSAGE)
+ url3 = re.compile(API_URL + "devices/fake_id/messages*")
+ mock_req.get(url3, text=VALID_MESSAGE)
- assert setup_component(self.hass, "sensor", VALID_CONFIG)
- self.hass.block_till_done()
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
- assert len(self.hass.states.entity_ids()) == 1
- state = self.hass.states.get("sensor.sigfox_fake_id")
- assert state.state == "payload"
- assert state.attributes.get("snr") == "50.0"
+ assert len(hass.states.async_entity_ids()) == 1
+ state = hass.states.get("sensor.sigfox_fake_id")
+ assert state.state == "payload"
+ assert state.attributes.get("snr") == "50.0"
diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py
index 86cd0fc384cbdc..e3d0b0479c4a09 100644
--- a/tests/components/simplisafe/test_config_flow.py
+++ b/tests/components/simplisafe/test_config_flow.py
@@ -10,7 +10,7 @@
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
-from tests.async_mock import MagicMock, PropertyMock, patch
+from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch
from tests.common import MockConfigEntry
@@ -49,7 +49,7 @@ async def test_invalid_credentials(hass):
with patch(
"simplipy.API.login_via_credentials",
- side_effect=InvalidCredentialsError,
+ new=AsyncMock(side_effect=InvalidCredentialsError),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
@@ -105,7 +105,9 @@ async def test_step_import(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
+ ), patch(
+ "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ ):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
@@ -140,7 +142,9 @@ async def test_step_reauth(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
+ ), patch(
+ "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ ):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"}
)
@@ -160,7 +164,9 @@ async def test_step_user(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
+ ), patch(
+ "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ ):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
@@ -183,7 +189,8 @@ async def test_step_user_mfa(hass):
}
with patch(
- "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError
+ "simplipy.API.login_via_credentials",
+ new=AsyncMock(side_effect=PendingAuthorizationError),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
@@ -191,7 +198,8 @@ async def test_step_user_mfa(hass):
assert result["step_id"] == "mfa"
with patch(
- "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError
+ "simplipy.API.login_via_credentials",
+ new=AsyncMock(side_effect=PendingAuthorizationError),
):
# Simulate the user pressing the MFA submit button without having clicked
# the link in the MFA email:
@@ -202,7 +210,9 @@ async def test_step_user_mfa(hass):
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
- ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
+ ), patch(
+ "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api())
+ ):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
@@ -222,7 +232,7 @@ async def test_unknown_error(hass):
with patch(
"simplipy.API.login_via_credentials",
- side_effect=SimplipyError,
+ new=AsyncMock(side_effect=SimplipyError),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
diff --git a/tests/components/simulated/test_sensor.py b/tests/components/simulated/test_sensor.py
index 09e77f7b283a08..781b5d494baddc 100644
--- a/tests/components/simulated/test_sensor.py
+++ b/tests/components/simulated/test_sensor.py
@@ -1,6 +1,4 @@
"""The tests for the simulated sensor."""
-import unittest
-
from homeassistant.components.simulated.sensor import (
CONF_AMP,
CONF_FWHM,
@@ -19,37 +17,24 @@
DEFAULT_SEED,
)
from homeassistant.const import CONF_FRIENDLY_NAME
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
-
-
-class TestSimulatedSensor(unittest.TestCase):
- """Test the simulated sensor."""
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_default_config(self):
- """Test default config."""
- config = {"sensor": {"platform": "simulated"}}
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
-
- assert len(self.hass.states.entity_ids()) == 1
- state = self.hass.states.get("sensor.simulated")
-
- assert state.attributes.get(CONF_FRIENDLY_NAME) == DEFAULT_NAME
- assert state.attributes.get(CONF_AMP) == DEFAULT_AMP
- assert state.attributes.get(CONF_UNIT) is None
- assert state.attributes.get(CONF_MEAN) == DEFAULT_MEAN
- assert state.attributes.get(CONF_PERIOD) == 60.0
- assert state.attributes.get(CONF_PHASE) == DEFAULT_PHASE
- assert state.attributes.get(CONF_FWHM) == DEFAULT_FWHM
- assert state.attributes.get(CONF_SEED) == DEFAULT_SEED
- assert state.attributes.get(CONF_RELATIVE_TO_EPOCH) == DEFAULT_RELATIVE_TO_EPOCH
+from homeassistant.setup import async_setup_component
+
+
+async def test_simulated_sensor_default_config(hass):
+ """Test default config."""
+ config = {"sensor": {"platform": "simulated"}}
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids()) == 1
+ state = hass.states.get("sensor.simulated")
+
+ assert state.attributes.get(CONF_FRIENDLY_NAME) == DEFAULT_NAME
+ assert state.attributes.get(CONF_AMP) == DEFAULT_AMP
+ assert state.attributes.get(CONF_UNIT) is None
+ assert state.attributes.get(CONF_MEAN) == DEFAULT_MEAN
+ assert state.attributes.get(CONF_PERIOD) == 60.0
+ assert state.attributes.get(CONF_PHASE) == DEFAULT_PHASE
+ assert state.attributes.get(CONF_FWHM) == DEFAULT_FWHM
+ assert state.attributes.get(CONF_SEED) == DEFAULT_SEED
+ assert state.attributes.get(CONF_RELATIVE_TO_EPOCH) == DEFAULT_RELATIVE_TO_EPOCH
diff --git a/tests/components/slack/__init__.py b/tests/components/slack/__init__.py
new file mode 100644
index 00000000000000..b32ec5ef7b19ed
--- /dev/null
+++ b/tests/components/slack/__init__.py
@@ -0,0 +1 @@
+"""Slack notification tests."""
diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py
new file mode 100644
index 00000000000000..bfd78b20900b51
--- /dev/null
+++ b/tests/components/slack/test_notify.py
@@ -0,0 +1,66 @@
+"""Test slack notifications."""
+from unittest.mock import Mock
+
+from homeassistant.components.slack.notify import SlackNotificationService
+
+from tests.async_mock import AsyncMock
+
+
+async def test_message_includes_default_emoji():
+ """Tests that default icon is used when no message icon is given."""
+ mock_client = Mock()
+ mock_client.chat_postMessage = AsyncMock()
+ expected_icon = ":robot_face:"
+ service = SlackNotificationService(None, mock_client, "_", "_", expected_icon)
+
+ await service.async_send_message("test")
+
+ mock_fn = mock_client.chat_postMessage
+ mock_fn.assert_called_once()
+ _, kwargs = mock_fn.call_args
+ assert kwargs["icon_emoji"] == expected_icon
+
+
+async def test_message_emoji_overrides_default():
+ """Tests that overriding the default icon emoji when sending a message works."""
+ mock_client = Mock()
+ mock_client.chat_postMessage = AsyncMock()
+ service = SlackNotificationService(None, mock_client, "_", "_", "default_icon")
+
+ expected_icon = ":new:"
+ await service.async_send_message("test", data={"icon": expected_icon})
+
+ mock_fn = mock_client.chat_postMessage
+ mock_fn.assert_called_once()
+ _, kwargs = mock_fn.call_args
+ assert kwargs["icon_emoji"] == expected_icon
+
+
+async def test_message_includes_default_icon_url():
+ """Tests that overriding the default icon url when sending a message works."""
+ mock_client = Mock()
+ mock_client.chat_postMessage = AsyncMock()
+ expected_icon = "https://example.com/hass.png"
+ service = SlackNotificationService(None, mock_client, "_", "_", expected_icon)
+
+ await service.async_send_message("test")
+
+ mock_fn = mock_client.chat_postMessage
+ mock_fn.assert_called_once()
+ _, kwargs = mock_fn.call_args
+ assert kwargs["icon_url"] == expected_icon
+
+
+async def test_message_icon_url_overrides_default():
+ """Tests that overriding the default icon url when sending a message works."""
+ mock_client = Mock()
+ mock_client.chat_postMessage = AsyncMock()
+ service = SlackNotificationService(None, mock_client, "_", "_", "default_icon")
+
+ expected_icon = "https://example.com/hass.png"
+ await service.async_send_message("test", data={"icon": expected_icon})
+
+ mock_fn = mock_client.chat_postMessage
+ mock_fn.assert_called_once()
+ _, kwargs = mock_fn.call_args
+ assert kwargs["icon_url"] == expected_icon
diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py
index 2c317520a7da1d..f12af1f38498d1 100644
--- a/tests/components/sma/test_sensor.py
+++ b/tests/components/sma/test_sensor.py
@@ -2,7 +2,7 @@
import logging
from homeassistant.components.sensor import DOMAIN
-from homeassistant.const import VOLT
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, VOLT
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component
@@ -28,7 +28,7 @@ async def test_sma_config(hass):
state = hass.states.get("sensor.current_consumption")
assert state
- assert "unit_of_measurement" in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT in state.attributes
assert "current_consumption" not in state.attributes
state = hass.states.get("sensor.my_sensor")
diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py
index 2200dd66490ecd..7434d469f96065 100644
--- a/tests/components/smappee/test_config_flow.py
+++ b/tests/components/smappee/test_config_flow.py
@@ -72,7 +72,7 @@ async def test_show_zeroconf_connection_error_form(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
@@ -95,7 +95,7 @@ async def test_connection_error(hass):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": "1.2.3.4"}
)
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py
index 1ccb15714b19bc..861425601ece05 100644
--- a/tests/components/smart_meter_texas/test_init.py
+++ b/tests/components/smart_meter_texas/test_init.py
@@ -45,6 +45,7 @@ async def test_update_failure(hass, config_entry, aioclient_mock):
"""Test that the coordinator handles a bad response."""
await setup_integration(hass, config_entry, aioclient_mock, bad_reading=True)
await async_setup_component(hass, HA_DOMAIN, {})
+ await hass.async_block_till_done()
with patch("smart_meter_texas.Meter.read_meter") as updater:
await hass.services.async_call(
HA_DOMAIN,
diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index b1df938b3b1d8f..89b4fbe9b13f57 100644
--- a/tests/components/somfy/test_config_flow.py
+++ b/tests/components/somfy/test_config_flow.py
@@ -49,7 +49,7 @@ async def test_abort_if_existing_entry(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py
index b076f39d0d25cb..a66184887d5748 100644
--- a/tests/components/sonarr/__init__.py
+++ b/tests/components/sonarr/__init__.py
@@ -15,6 +15,7 @@
CONF_PORT,
CONF_SSL,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
from homeassistant.helpers.typing import HomeAssistantType
@@ -34,6 +35,8 @@
"days": 3,
}
+MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"}
+
MOCK_USER_INPUT = {
CONF_HOST: HOST,
CONF_PORT: PORT,
@@ -85,43 +88,43 @@ def mock_connection(
aioclient_mock.get(
f"{sonarr_url}/system/status",
text=load_fixture("sonarr/system-status.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/diskspace",
text=load_fixture("sonarr/diskspace.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/calendar",
text=load_fixture("sonarr/calendar.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/command",
text=load_fixture("sonarr/command.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/queue",
text=load_fixture("sonarr/queue.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/series",
text=load_fixture("sonarr/series.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"{sonarr_url}/wanted/missing",
text=load_fixture("sonarr/wanted-missing.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py
index 15bd13b580d610..2c39e4384e5702 100644
--- a/tests/components/sonarr/test_config_flow.py
+++ b/tests/components/sonarr/test_config_flow.py
@@ -6,8 +6,8 @@
DEFAULT_WANTED_MAX_ITEMS,
DOMAIN,
)
-from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
-from homeassistant.const import CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
+from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
@@ -18,6 +18,7 @@
from tests.async_mock import patch
from tests.components.sonarr import (
HOST,
+ MOCK_REAUTH_INPUT,
MOCK_USER_INPUT,
_patch_async_setup,
_patch_async_setup_entry,
@@ -98,7 +99,7 @@ async def test_unknown_error(
async def test_full_import_flow_implementation(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
- """Test the full manual user flow from start to finish."""
+ """Test the full manual import flow from start to finish."""
mock_connection(aioclient_mock)
user_input = MOCK_USER_INPUT.copy()
@@ -117,6 +118,44 @@ async def test_full_import_flow_implementation(
assert result["data"][CONF_HOST] == HOST
+async def test_full_reauth_flow_implementation(
+ hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the manual reauth flow from start to finish."""
+ entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True)
+ assert entry
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_REAUTH},
+ data={"config_entry_id": entry.entry_id, **entry.data},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ user_input = MOCK_REAUTH_INPUT.copy()
+ with _patch_async_setup(), _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=user_input
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "reauth_successful"
+
+ assert entry.data[CONF_API_KEY] == "test-api-key-reauth"
+
+ mock_setup_entry.assert_called_once()
+
+
async def test_full_user_flow_implementation(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
@@ -180,7 +219,9 @@ async def test_full_user_flow_advanced_options(
async def test_options_flow(hass, aioclient_mock: AiohttpClientMocker):
"""Test updating options."""
- entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True)
+ with patch("homeassistant.components.sonarr.PLATFORMS", []):
+ entry = await setup_integration(hass, aioclient_mock)
+
assert entry.options[CONF_UPCOMING_DAYS] == DEFAULT_UPCOMING_DAYS
assert entry.options[CONF_WANTED_MAX_ITEMS] == DEFAULT_WANTED_MAX_ITEMS
@@ -194,6 +235,7 @@ async def test_options_flow(hass, aioclient_mock: AiohttpClientMocker):
result["flow_id"],
user_input={CONF_UPCOMING_DAYS: 2, CONF_WANTED_MAX_ITEMS: 100},
)
+ await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_UPCOMING_DAYS] == 2
diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py
index e9f01290461bf4..258be0203bb2c8 100644
--- a/tests/components/sonarr/test_init.py
+++ b/tests/components/sonarr/test_init.py
@@ -3,8 +3,11 @@
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
+ SOURCE_REAUTH,
)
+from homeassistant.const import CONF_SOURCE
from homeassistant.core import HomeAssistant
from tests.async_mock import patch
@@ -20,6 +23,22 @@ async def test_config_entry_not_ready(
assert entry.state == ENTRY_STATE_SETUP_RETRY
+async def test_config_entry_reauth(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the configuration entry needing to be re-authenticated."""
+ with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
+ entry = await setup_integration(hass, aioclient_mock, invalid_auth=True)
+
+ assert entry.state == ENTRY_STATE_SETUP_ERROR
+
+ mock_flow_init.assert_called_once_with(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_REAUTH},
+ data={"config_entry_id": entry.entry_id, **entry.data},
+ )
+
+
async def test_unload_config_entry(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py
index b831de56b0552c..0f33434254c605 100644
--- a/tests/components/spaceapi/test_init.py
+++ b/tests/components/spaceapi/test_init.py
@@ -5,7 +5,7 @@
import pytest
from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS
from homeassistant.setup import async_setup_component
from tests.common import mock_coro
@@ -76,13 +76,13 @@ def mock_client(hass, hass_client):
hass.loop.run_until_complete(async_setup_component(hass, "spaceapi", CONFIG))
hass.states.async_set(
- "test.temp1", 25, attributes={"unit_of_measurement": TEMP_CELSIUS}
+ "test.temp1", 25, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
)
hass.states.async_set(
- "test.temp2", 23, attributes={"unit_of_measurement": TEMP_CELSIUS}
+ "test.temp2", 23, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
)
hass.states.async_set(
- "test.hum1", 88, attributes={"unit_of_measurement": PERCENTAGE}
+ "test.hum1", 88, attributes={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}
)
return hass.loop.run_until_complete(hass_client())
diff --git a/tests/components/speedtestdotnet/__init__.py b/tests/components/speedtestdotnet/__init__.py
index f67a633e25f4fd..f6f64b9c7bb151 100644
--- a/tests/components/speedtestdotnet/__init__.py
+++ b/tests/components/speedtestdotnet/__init__.py
@@ -9,7 +9,7 @@
"name": "Server1",
"country": "Country1",
"cc": "LL1",
- "sponsor": "Server1",
+ "sponsor": "Sponsor1",
"id": "1",
"host": "server1:8080",
"d": 1,
@@ -23,7 +23,7 @@
"name": "Server2",
"country": "Country2",
"cc": "LL2",
- "sponsor": "server2",
+ "sponsor": "Sponsor2",
"id": "2",
"host": "server2:8080",
"d": 2,
diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py
index cfd79fb38f8d88..d421113a2d7dc5 100644
--- a/tests/components/speedtestdotnet/test_config_flow.py
+++ b/tests/components/speedtestdotnet/test_config_flow.py
@@ -108,7 +108,7 @@ async def test_options(hass):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
- CONF_SERVER_NAME: "Country1 - Server1",
+ CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: False,
},
@@ -116,7 +116,7 @@ async def test_options(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
- CONF_SERVER_NAME: "Country1 - Server1",
+ CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: False,
@@ -135,4 +135,4 @@ async def test_integration_already_configured(hass):
speedtestdotnet.DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/splunk/__init__.py b/tests/components/splunk/__init__.py
deleted file mode 100644
index 709483291e325b..00000000000000
--- a/tests/components/splunk/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the splunk component."""
diff --git a/tests/components/splunk/test_init.py b/tests/components/splunk/test_init.py
deleted file mode 100644
index 86de865bc0da24..00000000000000
--- a/tests/components/splunk/test_init.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""The tests for the Splunk component."""
-import json
-import unittest
-from unittest import mock
-
-import homeassistant.components.splunk as splunk
-from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON
-from homeassistant.core import State
-from homeassistant.helpers import state as state_helper
-from homeassistant.setup import setup_component
-import homeassistant.util.dt as dt_util
-
-from tests.common import get_test_home_assistant, mock_state_change_event
-
-
-class TestSplunk(unittest.TestCase):
- """Test the Splunk component."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_setup_config_full(self):
- """Test setup with all data."""
- config = {
- "splunk": {
- "host": "host",
- "port": 123,
- "token": "secret",
- "ssl": "False",
- "verify_ssl": "True",
- "name": "hostname",
- "filter": {
- "exclude_domains": ["fake"],
- "exclude_entities": ["fake.entity"],
- },
- }
- }
-
- self.hass.bus.listen = mock.MagicMock()
- assert setup_component(self.hass, splunk.DOMAIN, config)
- assert self.hass.bus.listen.called
- assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0]
-
- def test_setup_config_defaults(self):
- """Test setup with defaults."""
- config = {"splunk": {"host": "host", "token": "secret"}}
-
- self.hass.bus.listen = mock.MagicMock()
- assert setup_component(self.hass, splunk.DOMAIN, config)
- assert self.hass.bus.listen.called
- assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0]
-
- def _setup(self, mock_requests):
- """Test the setup."""
- # pylint: disable=attribute-defined-outside-init
- self.mock_post = mock_requests.post
- self.mock_request_exception = Exception
- mock_requests.exceptions.RequestException = self.mock_request_exception
- config = {"splunk": {"host": "host", "token": "secret", "port": 8088}}
-
- self.hass.bus.listen = mock.MagicMock()
- setup_component(self.hass, splunk.DOMAIN, config)
- self.handler_method = self.hass.bus.listen.call_args_list[0][0][1]
-
- @mock.patch.object(splunk, "requests")
- def test_event_listener(self, mock_requests):
- """Test event listener."""
- self._setup(mock_requests)
-
- now = dt_util.now()
- valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0, "foo": "foo"}
-
- for in_, out in valid.items():
- state = mock.MagicMock(
- state=in_,
- domain="fake",
- object_id="entity",
- attributes={"datetime_attr": now},
- )
- event = mock.MagicMock(data={"new_state": state}, time_fired=12345)
-
- try:
- out = state_helper.state_as_number(state)
- except ValueError:
- out = state.state
-
- body = [
- {
- "domain": "fake",
- "entity_id": "entity",
- "attributes": {"datetime_attr": now.isoformat()},
- "time": "12345",
- "value": out,
- "host": "HASS",
- }
- ]
-
- payload = {
- "host": "http://host:8088/services/collector/event",
- "event": body,
- }
- self.handler_method(event)
- assert self.mock_post.call_count == 1
- assert self.mock_post.call_args == mock.call(
- payload["host"],
- data=json.dumps(payload),
- headers={"Authorization": "Splunk secret"},
- timeout=10,
- verify=True,
- )
- self.mock_post.reset_mock()
-
- def _setup_with_filter(self, addl_filters=None):
- """Test the setup."""
- config = {
- "splunk": {
- "host": "host",
- "token": "secret",
- "port": 8088,
- "filter": {
- "exclude_domains": ["excluded_domain"],
- "exclude_entities": ["other_domain.excluded_entity"],
- },
- }
- }
- if addl_filters:
- config["splunk"]["filter"].update(addl_filters)
-
- setup_component(self.hass, splunk.DOMAIN, config)
-
- @mock.patch.object(splunk, "post_request")
- def test_splunk_entityfilter(self, mock_requests):
- """Test event listener."""
- # pylint: disable=no-member
- self._setup_with_filter()
-
- testdata = [
- {"entity_id": "other_domain.other_entity", "filter_expected": False},
- {"entity_id": "other_domain.excluded_entity", "filter_expected": True},
- {"entity_id": "excluded_domain.other_entity", "filter_expected": True},
- ]
-
- for test in testdata:
- mock_state_change_event(self.hass, State(test["entity_id"], "on"))
- self.hass.block_till_done()
-
- if test["filter_expected"]:
- assert not splunk.post_request.called
- else:
- assert splunk.post_request.called
-
- splunk.post_request.reset_mock()
-
- @mock.patch.object(splunk, "post_request")
- def test_splunk_entityfilter_with_glob_filter(self, mock_requests):
- """Test event listener."""
- # pylint: disable=no-member
- self._setup_with_filter({"exclude_entity_globs": ["*.skip_*"]})
-
- testdata = [
- {"entity_id": "other_domain.other_entity", "filter_expected": False},
- {"entity_id": "other_domain.excluded_entity", "filter_expected": True},
- {"entity_id": "excluded_domain.other_entity", "filter_expected": True},
- {"entity_id": "test.skip_me", "filter_expected": True},
- ]
-
- for test in testdata:
- mock_state_change_event(self.hass, State(test["entity_id"], "on"))
- self.hass.block_till_done()
-
- if test["filter_expected"]:
- assert not splunk.post_request.called
- else:
- assert splunk.post_request.called
-
- splunk.post_request.reset_mock()
diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py
index 8b8bca5e37c9b9..ddab7b1ba36152 100644
--- a/tests/components/sql/test_sensor.py
+++ b/tests/components/sql/test_sensor.py
@@ -1,71 +1,57 @@
"""The test for the sql sensor platform."""
-import unittest
-
import pytest
import voluptuous as vol
from homeassistant.components.sql.sensor import validate_sql_select
from homeassistant.const import STATE_UNKNOWN
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
+from homeassistant.setup import async_setup_component
-class TestSQLSensor(unittest.TestCase):
+async def test_query(hass):
"""Test the SQL sensor."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_query(self):
- """Test the SQL sensor."""
- config = {
- "sensor": {
- "platform": "sql",
- "db_url": "sqlite://",
- "queries": [
- {
- "name": "count_tables",
- "query": "SELECT 5 as value",
- "column": "value",
- }
- ],
- }
+ config = {
+ "sensor": {
+ "platform": "sql",
+ "db_url": "sqlite://",
+ "queries": [
+ {
+ "name": "count_tables",
+ "query": "SELECT 5 as value",
+ "column": "value",
+ }
+ ],
}
-
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.count_tables")
- assert state.state == "5"
- assert state.attributes["value"] == 5
-
- def test_invalid_query(self):
- """Test the SQL sensor for invalid queries."""
- with pytest.raises(vol.Invalid):
- validate_sql_select("DROP TABLE *")
-
- config = {
- "sensor": {
- "platform": "sql",
- "db_url": "sqlite://",
- "queries": [
- {
- "name": "count_tables",
- "query": "SELECT * value FROM sqlite_master;",
- "column": "value",
- }
- ],
- }
+ }
+
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.count_tables")
+ assert state.state == "5"
+ assert state.attributes["value"] == 5
+
+
+async def test_invalid_query(hass):
+ """Test the SQL sensor for invalid queries."""
+ with pytest.raises(vol.Invalid):
+ validate_sql_select("DROP TABLE *")
+
+ config = {
+ "sensor": {
+ "platform": "sql",
+ "db_url": "sqlite://",
+ "queries": [
+ {
+ "name": "count_tables",
+ "query": "SELECT * value FROM sqlite_master;",
+ "column": "value",
+ }
+ ],
}
+ }
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.count_tables")
- assert state.state == STATE_UNKNOWN
+ state = hass.states.get("sensor.count_tables")
+ assert state.state == STATE_UNKNOWN
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index 6e36778b75d4b9..b6c8266b5da910 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -15,7 +15,14 @@ async def test_scan_match_st(hass):
scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
with patch(
- "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location=None)]
+ "netdisco.ssdp.scan",
+ return_value=[
+ Mock(
+ st="mock-st",
+ location=None,
+ values={"usn": "mock-usn", "server": "mock-server", "ext": ""},
+ )
+ ],
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -24,6 +31,13 @@ async def test_scan_match_st(hass):
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ ssdp.ATTR_SSDP_ST: "mock-st",
+ ssdp.ATTR_SSDP_LOCATION: None,
+ ssdp.ATTR_SSDP_USN: "mock-usn",
+ ssdp.ATTR_SSDP_SERVER: "mock-server",
+ ssdp.ATTR_SSDP_EXT: "",
+ }
@pytest.mark.parametrize(
@@ -45,7 +59,7 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key):
with patch(
"netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
+ return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -82,7 +96,7 @@ async def test_scan_not_all_present(hass, aioclient_mock):
with patch(
"netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
+ return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -118,7 +132,7 @@ async def test_scan_not_all_match(hass, aioclient_mock):
with patch(
"netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
+ return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -135,7 +149,7 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc):
with patch(
"netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
+ return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
):
await scanner.async_scan(None)
@@ -152,6 +166,6 @@ async def test_scan_description_parse_fail(hass, aioclient_mock):
with patch(
"netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
+ return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
):
await scanner.async_scan(None)
diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py
index e1d658d05b7a89..511061933cb130 100644
--- a/tests/components/startca/test_sensor.py
+++ b/tests/components/startca/test_sensor.py
@@ -1,7 +1,12 @@
"""Tests for the Start.ca sensor platform."""
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.startca.sensor import StartcaData
-from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, PERCENTAGE
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ DATA_GIGABYTES,
+ HTTP_NOT_FOUND,
+ PERCENTAGE,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -53,51 +58,51 @@ async def test_capped_setup(hass, aioclient_mock):
await hass.async_block_till_done()
state = hass.states.get("sensor.start_ca_usage_ratio")
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "76.24"
state = hass.states.get("sensor.start_ca_usage")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_data_limit")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "400"
state = hass.states.get("sensor.start_ca_used_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_used_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_used_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_grace_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_grace_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_grace_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_total_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_total_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_remaining")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "95.05"
@@ -149,51 +154,51 @@ async def test_unlimited_setup(hass, aioclient_mock):
await hass.async_block_till_done()
state = hass.states.get("sensor.start_ca_usage_ratio")
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.state == "0"
state = hass.states.get("sensor.start_ca_usage")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_data_limit")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "inf"
state = hass.states.get("sensor.start_ca_used_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_used_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_used_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "0.0"
state = hass.states.get("sensor.start_ca_grace_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_grace_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_grace_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "311.43"
state = hass.states.get("sensor.start_ca_total_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "304.95"
state = hass.states.get("sensor.start_ca_total_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "6.48"
state = hass.states.get("sensor.start_ca_remaining")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES
assert state.state == "inf"
diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py
index e60c5c2e9a5838..24401963974ad9 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -115,7 +115,7 @@ def test_sensor_source(self):
assert self.mean == state.attributes.get("mean")
assert self.count == state.attributes.get("count")
assert self.total == state.attributes.get("total")
- assert TEMP_CELSIUS == state.attributes.get("unit_of_measurement")
+ assert TEMP_CELSIUS == state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
assert self.change == state.attributes.get("change")
assert self.average_change == state.attributes.get("average_change")
diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py
index 9564de85a978e8..30b52659685ab3 100644
--- a/tests/components/statsd/test_init.py
+++ b/tests/components/statsd/test_init.py
@@ -1,5 +1,4 @@
"""The tests for the StatsD feeder."""
-import unittest
from unittest import mock
import pytest
@@ -8,133 +7,128 @@
import homeassistant.components.statsd as statsd
from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON
import homeassistant.core as ha
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant
+from tests.async_mock import MagicMock, patch
-class TestStatsd(unittest.TestCase):
- """Test the StatsD component."""
+@pytest.fixture
+def mock_client():
+ """Pytest fixture for statsd library."""
+ with patch("statsd.StatsClient") as mock_client:
+ yield mock_client.return_value
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
+def test_invalid_config():
+ """Test configuration with defaults."""
+ config = {"statsd": {"host1": "host1"}}
- def test_invalid_config(self):
- """Test configuration with defaults."""
- config = {"statsd": {"host1": "host1"}}
+ with pytest.raises(vol.Invalid):
+ statsd.CONFIG_SCHEMA(None)
+ with pytest.raises(vol.Invalid):
+ statsd.CONFIG_SCHEMA(config)
- with pytest.raises(vol.Invalid):
- statsd.CONFIG_SCHEMA(None)
- with pytest.raises(vol.Invalid):
- statsd.CONFIG_SCHEMA(config)
- @mock.patch("statsd.StatsClient")
- def test_statsd_setup_full(self, mock_connection):
- """Test setup with all data."""
- config = {"statsd": {"host": "host", "port": 123, "rate": 1, "prefix": "foo"}}
- self.hass.bus.listen = mock.MagicMock()
- assert setup_component(self.hass, statsd.DOMAIN, config)
- assert mock_connection.call_count == 1
- assert mock_connection.call_args == mock.call(
- host="host", port=123, prefix="foo"
+async def test_statsd_setup_full(hass):
+ """Test setup with all data."""
+ config = {"statsd": {"host": "host", "port": 123, "rate": 1, "prefix": "foo"}}
+ hass.bus.listen = MagicMock()
+ with patch("statsd.StatsClient") as mock_init:
+ assert await async_setup_component(hass, statsd.DOMAIN, config)
+
+ assert mock_init.call_count == 1
+ assert mock_init.call_args == mock.call(host="host", port=123, prefix="foo")
+
+ assert hass.bus.listen.called
+ assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+
+
+async def test_statsd_setup_defaults(hass):
+ """Test setup with defaults."""
+ config = {"statsd": {"host": "host"}}
+
+ config["statsd"][statsd.CONF_PORT] = statsd.DEFAULT_PORT
+ config["statsd"][statsd.CONF_PREFIX] = statsd.DEFAULT_PREFIX
+
+ hass.bus.listen = MagicMock()
+ with patch("statsd.StatsClient") as mock_init:
+ assert await async_setup_component(hass, statsd.DOMAIN, config)
+
+ assert mock_init.call_count == 1
+ assert mock_init.call_args == mock.call(host="host", port=8125, prefix="hass")
+ assert hass.bus.listen.called
+
+
+async def test_event_listener_defaults(hass, mock_client):
+ """Test event listener."""
+ config = {"statsd": {"host": "host", "value_mapping": {"custom": 3}}}
+
+ config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE
+
+ hass.bus.listen = MagicMock()
+ await async_setup_component(hass, statsd.DOMAIN, config)
+ assert hass.bus.listen.called
+ handler_method = hass.bus.listen.call_args_list[0][0][1]
+
+ valid = {"1": 1, "1.0": 1.0, "custom": 3, STATE_ON: 1, STATE_OFF: 0}
+ for in_, out in valid.items():
+ state = MagicMock(state=in_, attributes={"attribute key": 3.2})
+ handler_method(MagicMock(data={"new_state": state}))
+ mock_client.gauge.assert_has_calls(
+ [mock.call(state.entity_id, out, statsd.DEFAULT_RATE)]
)
- assert self.hass.bus.listen.called
- assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0]
+ mock_client.gauge.reset_mock()
- @mock.patch("statsd.StatsClient")
- def test_statsd_setup_defaults(self, mock_connection):
- """Test setup with defaults."""
- config = {"statsd": {"host": "host"}}
+ assert mock_client.incr.call_count == 1
+ assert mock_client.incr.call_args == mock.call(
+ state.entity_id, rate=statsd.DEFAULT_RATE
+ )
+ mock_client.incr.reset_mock()
- config["statsd"][statsd.CONF_PORT] = statsd.DEFAULT_PORT
- config["statsd"][statsd.CONF_PREFIX] = statsd.DEFAULT_PREFIX
+ for invalid in ("foo", "", object):
+ handler_method(
+ MagicMock(data={"new_state": ha.State("domain.test", invalid, {})})
+ )
+ assert not mock_client.gauge.called
+ assert mock_client.incr.called
+
+
+async def test_event_listener_attr_details(hass, mock_client):
+ """Test event listener."""
+ config = {"statsd": {"host": "host", "log_attributes": True}}
+
+ config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE
+
+ hass.bus.listen = MagicMock()
+ await async_setup_component(hass, statsd.DOMAIN, config)
+ assert hass.bus.listen.called
+ handler_method = hass.bus.listen.call_args_list[0][0][1]
+
+ valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0}
+ for in_, out in valid.items():
+ state = MagicMock(state=in_, attributes={"attribute key": 3.2})
+ handler_method(MagicMock(data={"new_state": state}))
+ mock_client.gauge.assert_has_calls(
+ [
+ mock.call("%s.state" % state.entity_id, out, statsd.DEFAULT_RATE),
+ mock.call(
+ "%s.attribute_key" % state.entity_id, 3.2, statsd.DEFAULT_RATE
+ ),
+ ]
+ )
+
+ mock_client.gauge.reset_mock()
+
+ assert mock_client.incr.call_count == 1
+ assert mock_client.incr.call_args == mock.call(
+ state.entity_id, rate=statsd.DEFAULT_RATE
+ )
+ mock_client.incr.reset_mock()
- self.hass.bus.listen = mock.MagicMock()
- assert setup_component(self.hass, statsd.DOMAIN, config)
- assert mock_connection.call_count == 1
- assert mock_connection.call_args == mock.call(
- host="host", port=8125, prefix="hass"
+ for invalid in ("foo", "", object):
+ handler_method(
+ MagicMock(data={"new_state": ha.State("domain.test", invalid, {})})
)
- assert self.hass.bus.listen.called
-
- @mock.patch("statsd.StatsClient")
- def test_event_listener_defaults(self, mock_client):
- """Test event listener."""
- config = {"statsd": {"host": "host", "value_mapping": {"custom": 3}}}
-
- config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE
-
- self.hass.bus.listen = mock.MagicMock()
- setup_component(self.hass, statsd.DOMAIN, config)
- assert self.hass.bus.listen.called
- handler_method = self.hass.bus.listen.call_args_list[0][0][1]
-
- valid = {"1": 1, "1.0": 1.0, "custom": 3, STATE_ON: 1, STATE_OFF: 0}
- for in_, out in valid.items():
- state = mock.MagicMock(state=in_, attributes={"attribute key": 3.2})
- handler_method(mock.MagicMock(data={"new_state": state}))
- mock_client.return_value.gauge.assert_has_calls(
- [mock.call(state.entity_id, out, statsd.DEFAULT_RATE)]
- )
-
- mock_client.return_value.gauge.reset_mock()
-
- assert mock_client.return_value.incr.call_count == 1
- assert mock_client.return_value.incr.call_args == mock.call(
- state.entity_id, rate=statsd.DEFAULT_RATE
- )
- mock_client.return_value.incr.reset_mock()
-
- for invalid in ("foo", "", object):
- handler_method(
- mock.MagicMock(data={"new_state": ha.State("domain.test", invalid, {})})
- )
- assert not mock_client.return_value.gauge.called
- assert mock_client.return_value.incr.called
-
- @mock.patch("statsd.StatsClient")
- def test_event_listener_attr_details(self, mock_client):
- """Test event listener."""
- config = {"statsd": {"host": "host", "log_attributes": True}}
-
- config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE
-
- self.hass.bus.listen = mock.MagicMock()
- setup_component(self.hass, statsd.DOMAIN, config)
- assert self.hass.bus.listen.called
- handler_method = self.hass.bus.listen.call_args_list[0][0][1]
-
- valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0}
- for in_, out in valid.items():
- state = mock.MagicMock(state=in_, attributes={"attribute key": 3.2})
- handler_method(mock.MagicMock(data={"new_state": state}))
- mock_client.return_value.gauge.assert_has_calls(
- [
- mock.call("%s.state" % state.entity_id, out, statsd.DEFAULT_RATE),
- mock.call(
- "%s.attribute_key" % state.entity_id, 3.2, statsd.DEFAULT_RATE
- ),
- ]
- )
-
- mock_client.return_value.gauge.reset_mock()
-
- assert mock_client.return_value.incr.call_count == 1
- assert mock_client.return_value.incr.call_args == mock.call(
- state.entity_id, rate=statsd.DEFAULT_RATE
- )
- mock_client.return_value.incr.reset_mock()
-
- for invalid in ("foo", "", object):
- handler_method(
- mock.MagicMock(data={"new_state": ha.State("domain.test", invalid, {})})
- )
- assert not mock_client.return_value.gauge.called
- assert mock_client.return_value.incr.called
+ assert not mock_client.gauge.called
+ assert mock_client.incr.called
diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py
index f730dae3cf1344..2cb21051200538 100644
--- a/tests/components/sun/test_trigger.py
+++ b/tests/components/sun/test_trigger.py
@@ -5,13 +5,19 @@
from homeassistant.components import sun
import homeassistant.components.automation as automation
-from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ENTITY_MATCH_ALL,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ SUN_EVENT_SUNRISE,
+ SUN_EVENT_SUNSET,
+)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.async_mock import patch
from tests.common import async_fire_time_changed, async_mock_service, mock_component
-from tests.components.automation import common
ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
@@ -54,16 +60,24 @@ async def test_sunset_trigger(hass, calls, legacy_patchable_time):
},
)
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_time_changed(hass, trigger_time)
await hass.async_block_till_done()
assert len(calls) == 0
with patch("homeassistant.util.dt.utcnow", return_value=now):
- await common.async_turn_on(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
async_fire_time_changed(hass, trigger_time)
await hass.async_block_till_done()
diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py
index 3034877c6d684a..e260ce83dbf529 100644
--- a/tests/components/switch/test_light.py
+++ b/tests/components/switch/test_light.py
@@ -56,6 +56,7 @@ async def test_light_service_calls(hass):
assert hass.states.get("light.light_switch").state == "on"
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"
@@ -74,11 +75,13 @@ async def test_switch_service_calls(hass):
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"
diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py
index 64527c709641dc..404653a035345d 100644
--- a/tests/components/synology_dsm/test_config_flow.py
+++ b/tests/components/synology_dsm/test_config_flow.py
@@ -19,6 +19,7 @@
DEFAULT_PORT_SSL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SSL,
+ DEFAULT_TIMEOUT,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
@@ -30,6 +31,7 @@
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.helpers.typing import HomeAssistantType
@@ -288,7 +290,7 @@ async def test_login_failed(hass: HomeAssistantType, service: MagicMock):
data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_USERNAME: "login"}
+ assert result["errors"] == {CONF_USERNAME: "invalid_auth"}
async def test_connection_failed(hass: HomeAssistantType, service: MagicMock):
@@ -304,7 +306,7 @@ async def test_connection_failed(hass: HomeAssistantType, service: MagicMock):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_HOST: "connection"}
+ assert result["errors"] == {CONF_HOST: "cannot_connect"}
async def test_unknown_failed(hass: HomeAssistantType, service: MagicMock):
@@ -426,12 +428,14 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL
+ assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT
# Manual
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
- user_input={CONF_SCAN_INTERVAL: 2},
+ user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options[CONF_SCAN_INTERVAL] == 2
+ assert config_entry.options[CONF_TIMEOUT] == 30
diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py
index d3f7447277cc5a..49cd2d8ea8bd0a 100644
--- a/tests/components/system_log/test_init.py
+++ b/tests/components/system_log/test_init.py
@@ -31,6 +31,8 @@ async def _async_block_until_queue_empty(hass, sq):
await hass.async_block_till_done()
while not sq.empty():
await asyncio.sleep(0.01)
+ hass.data[system_log.DOMAIN].acquire()
+ hass.data[system_log.DOMAIN].release()
await hass.async_block_till_done()
diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py
index d10a59ef2f092a..ecb927c1c5cd49 100644
--- a/tests/components/tag/test_init.py
+++ b/tests/components/tag/test_init.py
@@ -6,6 +6,9 @@
from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag
from homeassistant.helpers import collection
from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from tests.async_mock import patch
_LOGGER = logging.getLogger(__name__)
@@ -45,6 +48,30 @@ async def test_ws_list(hass, hass_ws_client, storage_setup):
assert "test tag" in result
+async def test_ws_update(hass, hass_ws_client, storage_setup):
+ """Test listing tags via WS."""
+ assert await storage_setup()
+ await async_scan_tag(hass, "test tag", "some_scanner")
+
+ client = await hass_ws_client(hass)
+
+ await client.send_json(
+ {
+ "id": 6,
+ "type": f"{DOMAIN}/update",
+ f"{DOMAIN}_id": "test tag",
+ "name": "New name",
+ }
+ )
+ resp = await client.receive_json()
+ assert resp["success"]
+
+ item = resp["result"]
+
+ assert item["id"] == "test tag"
+ assert item["name"] == "New name"
+
+
async def test_tag_scanned(hass, hass_ws_client, storage_setup):
"""Test scanning tags."""
assert await storage_setup()
@@ -60,7 +87,10 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup):
assert len(result) == 1
assert "test tag" in result
- await async_scan_tag(hass, "new tag", "some_scanner")
+ now = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ await async_scan_tag(hass, "new tag", "some_scanner")
+
await client.send_json({"id": 7, "type": f"{DOMAIN}/list"})
resp = await client.receive_json()
assert resp["success"]
@@ -70,7 +100,7 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup):
assert len(result) == 2
assert "test tag" in result
assert "new tag" in result
- assert result["new tag"]["last_scanned"] is not None
+ assert result["new tag"]["last_scanned"] == now.isoformat()
def track_changes(coll: collection.ObservableCollection):
diff --git a/tests/components/tasmota/__init__.py b/tests/components/tasmota/__init__.py
new file mode 100644
index 00000000000000..201e9c3df1821c
--- /dev/null
+++ b/tests/components/tasmota/__init__.py
@@ -0,0 +1 @@
+"""Tests for Tasmota component."""
diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py
new file mode 100644
index 00000000000000..9a1907ecf3fc9e
--- /dev/null
+++ b/tests/components/tasmota/conftest.py
@@ -0,0 +1,48 @@
+"""Test fixtures for Tasmota component."""
+
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.tasmota.const import (
+ CONF_DISCOVERY_PREFIX,
+ DEFAULT_PREFIX,
+ DOMAIN,
+)
+
+from tests.common import MockConfigEntry, mock_device_registry, mock_registry
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+async def setup_tasmota_helper(hass):
+ """Set up Tasmota."""
+ hass.config.components.add("tasmota")
+
+ entry = MockConfigEntry(
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ data={CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX},
+ domain=DOMAIN,
+ title="Tasmota",
+ )
+
+ entry.add_to_hass(hass)
+
+ assert await hass.config_entries.async_setup(entry.entry_id)
+
+ assert "tasmota" in hass.config.components
+
+
+@pytest.fixture
+async def setup_tasmota(hass):
+ """Set up Tasmota."""
+ await setup_tasmota_helper(hass)
diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py
new file mode 100644
index 00000000000000..73a09a4844de12
--- /dev/null
+++ b/tests/components/tasmota/test_common.py
@@ -0,0 +1,360 @@
+"""Common test objects."""
+import copy
+import json
+
+from hatasmota.const import (
+ CONF_MAC,
+ CONF_OFFLINE,
+ CONF_ONLINE,
+ CONF_PREFIX,
+ PREFIX_CMND,
+ PREFIX_TELE,
+)
+from hatasmota.utils import (
+ config_get_state_offline,
+ config_get_state_online,
+ get_topic_tele_state,
+ get_topic_tele_will,
+)
+
+from homeassistant.components.tasmota.const import DEFAULT_PREFIX
+from homeassistant.const import STATE_UNAVAILABLE
+
+from tests.async_mock import ANY
+from tests.common import async_fire_mqtt_message
+
+DEFAULT_CONFIG = {
+ "ip": "192.168.15.10",
+ "dn": "Tasmota",
+ "fn": ["Test", "Beer", "Milk", "Four", None],
+ "hn": "tasmota_49A3BC-0956",
+ "lk": 1, # RGB + white channels linked to a single light
+ "mac": "00000049A3BC",
+ "md": "Sonoff Basic",
+ "ofln": "Offline",
+ "onln": "Online",
+ "state": ["OFF", "ON", "TOGGLE", "HOLD"],
+ "sw": "8.4.0.2",
+ "t": "tasmota_49A3BC",
+ "ft": "%topic%/%prefix%/",
+ "tp": ["cmnd", "stat", "tele"],
+ "rl": [0, 0, 0, 0, 0, 0, 0, 0],
+ "swc": [-1, -1, -1, -1, -1, -1, -1, -1],
+ "btn": [0, 0, 0, 0],
+ "so": {
+ "11": 0, # Swap button single and double press functionality
+ "13": 0, # Allow immediate action on single button press
+ "17": 1, # Show Color string as hex or comma-separated
+ "20": 0, # Update of Dimmer/Color/CT without turning power on
+ "30": 0, # Enforce Home Assistant auto-discovery as light
+ "68": 0, # Multi-channel PWM instead of a single light
+ "73": 0, # Enable Buttons decoupling and send multi-press and hold MQTT messages
+ "80": 0, # Blinds and shutters support
+ },
+ "lt_st": 0,
+ "ver": 1,
+}
+
+
+async def help_test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, domain, config
+):
+ """Test availability after MQTT disconnection."""
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(
+ hass,
+ get_topic_tele_will(config),
+ config_get_state_online(config),
+ )
+
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+ mqtt_mock.connected = False
+ await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ mqtt_mock.connected = True
+ await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+
+async def help_test_availability(
+ hass,
+ mqtt_mock,
+ domain,
+ config,
+):
+ """Test availability.
+
+ This is a test helper for the TasmotaAvailability mixin.
+ """
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(
+ hass,
+ get_topic_tele_will(config),
+ config_get_state_online(config),
+ )
+
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(
+ hass,
+ get_topic_tele_will(config),
+ config_get_state_offline(config),
+ )
+
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+
+async def help_test_availability_discovery_update(
+ hass,
+ mqtt_mock,
+ domain,
+ config,
+):
+ """Test update of discovered TasmotaAvailability.
+
+ This is a test helper for the TasmotaAvailability mixin.
+ """
+ # customize availability topic
+ config1 = copy.deepcopy(config)
+ config1[CONF_PREFIX][PREFIX_TELE] = "tele1"
+ config1[CONF_OFFLINE] = "offline1"
+ config1[CONF_ONLINE] = "online1"
+ config2 = copy.deepcopy(config)
+ config2[CONF_PREFIX][PREFIX_TELE] = "tele2"
+ config2[CONF_OFFLINE] = "offline2"
+ config2[CONF_ONLINE] = "online2"
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+
+ availability_topic1 = get_topic_tele_will(config1)
+ availability_topic2 = get_topic_tele_will(config2)
+ assert availability_topic1 != availability_topic2
+ offline1 = config_get_state_offline(config1)
+ offline2 = config_get_state_offline(config2)
+ assert offline1 != offline2
+ online1 = config_get_state_online(config1)
+ online2 = config_get_state_online(config2)
+ assert online1 != online2
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(hass, availability_topic1, online1)
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(hass, availability_topic1, offline1)
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ # Change availability settings
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/config", data2)
+ await hass.async_block_till_done()
+
+ # Verify we are no longer subscribing to the old topic or payload
+ async_fire_mqtt_message(hass, availability_topic1, online1)
+ async_fire_mqtt_message(hass, availability_topic1, online2)
+ async_fire_mqtt_message(hass, availability_topic2, online1)
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ # Verify we are subscribing to the new topic
+ async_fire_mqtt_message(hass, availability_topic2, online2)
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+
+async def help_test_discovery_removal(
+ hass, mqtt_mock, caplog, domain, config1, config2
+):
+ """Test removal of discovered entity."""
+ device_reg = await hass.helpers.device_registry.async_get_registry()
+ entity_reg = await hass.helpers.entity_registry.async_get_registry()
+
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+ assert config1[CONF_MAC] == config2[CONF_MAC]
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1)
+ await hass.async_block_till_done()
+
+ # Verify device and entity registry entries are created
+ device_entry = device_reg.async_get_device(set(), {("mac", config1[CONF_MAC])})
+ assert device_entry is not None
+ entity_entry = entity_reg.async_get(f"{domain}.test")
+ assert entity_entry is not None
+
+ # Verify state is added
+ state = hass.states.get(f"{domain}.test")
+ assert state is not None
+ assert state.name == "Test"
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/config", data2)
+ await hass.async_block_till_done()
+
+ # Verify entity registry entries are cleared
+ device_entry = device_reg.async_get_device(set(), {("mac", config2[CONF_MAC])})
+ assert device_entry is not None
+ entity_entry = entity_reg.async_get(f"{domain}.test")
+ assert entity_entry is None
+
+ # Verify state is removed
+ state = hass.states.get(f"{domain}.test")
+ assert state is None
+
+
+async def help_test_discovery_update_unchanged(
+ hass, mqtt_mock, caplog, domain, config, discovery_update
+):
+ """Test update of discovered component without changes.
+
+ This is a test helper for the MqttDiscoveryUpdate mixin.
+ """
+ config1 = copy.deepcopy(config)
+ config2 = copy.deepcopy(config)
+ config2[CONF_PREFIX][PREFIX_CMND] = "cmnd2"
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{domain}.test")
+ assert state is not None
+ assert state.name == "Test"
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
+ await hass.async_block_till_done()
+
+ assert not discovery_update.called
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data2)
+ await hass.async_block_till_done()
+
+ assert discovery_update.called
+
+
+async def help_test_discovery_device_remove(hass, mqtt_mock, domain, unique_id, config):
+ """Test domain entity is removed when device is removed."""
+ device_reg = await hass.helpers.device_registry.async_get_registry()
+ entity_reg = await hass.helpers.entity_registry.async_get_registry()
+
+ config = copy.deepcopy(config)
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
+ await hass.async_block_till_done()
+
+ device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])})
+ assert device is not None
+ assert entity_reg.async_get_entity_id(domain, "tasmota", unique_id)
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", "")
+ await hass.async_block_till_done()
+
+ device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])})
+ assert device is None
+ assert not entity_reg.async_get_entity_id(domain, "tasmota", unique_id)
+
+
+async def help_test_entity_id_update_subscriptions(
+ hass, mqtt_mock, domain, config, topics=None
+):
+ """Test MQTT subscriptions are managed when entity_id is updated."""
+ entity_reg = await hass.helpers.entity_registry.async_get_registry()
+
+ config = copy.deepcopy(config)
+ data = json.dumps(config)
+
+ mqtt_mock.async_subscribe.reset_mock()
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
+ await hass.async_block_till_done()
+
+ topics = [get_topic_tele_state(config), get_topic_tele_will(config)]
+ assert len(topics) > 0
+
+ state = hass.states.get(f"{domain}.test")
+ assert state is not None
+ assert mqtt_mock.async_subscribe.call_count == len(topics)
+ for topic in topics:
+ mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY)
+ mqtt_mock.async_subscribe.reset_mock()
+
+ entity_reg.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk")
+ await hass.async_block_till_done()
+
+ state = hass.states.get(f"{domain}.test")
+ assert state is None
+
+ state = hass.states.get(f"{domain}.milk")
+ assert state is not None
+ for topic in topics:
+ mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY)
+
+
+async def help_test_entity_id_update_discovery_update(hass, mqtt_mock, domain, config):
+ """Test MQTT discovery update after entity_id is updated."""
+ entity_reg = await hass.helpers.entity_registry.async_get_registry()
+
+ config = copy.deepcopy(config)
+ data = json.dumps(config)
+
+ topic = get_topic_tele_will(config)
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, topic, config_get_state_online(config))
+ state = hass.states.get(f"{domain}.test")
+ assert state.state != STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(hass, topic, config_get_state_offline(config))
+ state = hass.states.get(f"{domain}.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ entity_reg.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk")
+ await hass.async_block_till_done()
+ assert hass.states.get(f"{domain}.milk")
+
+ assert config[CONF_PREFIX][PREFIX_TELE] != "tele2"
+ config[CONF_PREFIX][PREFIX_TELE] = "tele2"
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(domain)) == 1
+
+ topic = get_topic_tele_will(config)
+ async_fire_mqtt_message(hass, topic, config_get_state_online(config))
+ state = hass.states.get(f"{domain}.milk")
+ assert state.state != STATE_UNAVAILABLE
diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py
new file mode 100644
index 00000000000000..ee66b486a5f4dd
--- /dev/null
+++ b/tests/components/tasmota/test_config_flow.py
@@ -0,0 +1,61 @@
+"""Test config flow."""
+
+from tests.common import MockConfigEntry
+
+
+async def test_user_setup(hass, mqtt_mock):
+ """Test we can finish a config flow."""
+ result = await hass.config_entries.flow.async_init(
+ "tasmota", context={"source": "user"}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ assert result["type"] == "create_entry"
+ assert result["result"].data == {
+ "discovery_prefix": "tasmota/discovery",
+ }
+
+
+async def test_user_setup_advanced(hass, mqtt_mock):
+ """Test we can finish a config flow."""
+ result = await hass.config_entries.flow.async_init(
+ "tasmota", context={"source": "user", "show_advanced_options": True}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"discovery_prefix": "test_tasmota/discovery"}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["result"].data == {
+ "discovery_prefix": "test_tasmota/discovery",
+ }
+
+
+async def test_user_setup_invalid_topic_prefix(hass, mqtt_mock):
+ """Test if connection cannot be made."""
+ result = await hass.config_entries.flow.async_init(
+ "tasmota", context={"source": "user", "show_advanced_options": True}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"discovery_prefix": "tasmota/config/#"}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"]["base"] == "invalid_discovery_topic"
+
+
+async def test_user_single_instance(hass, mqtt_mock):
+ """Test we only allow a single config flow."""
+ MockConfigEntry(domain="tasmota").add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ "tasmota", context={"source": "user"}
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py
new file mode 100644
index 00000000000000..9af22636c5353f
--- /dev/null
+++ b/tests/components/tasmota/test_discovery.py
@@ -0,0 +1,354 @@
+"""The tests for the MQTT discovery."""
+import copy
+import json
+
+import pytest
+
+from homeassistant.components.tasmota.const import DEFAULT_PREFIX
+from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED
+
+from .conftest import setup_tasmota_helper
+from .test_common import DEFAULT_CONFIG
+
+from tests.async_mock import patch
+from tests.common import async_fire_mqtt_message
+
+
+async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota):
+ """Test setting up discovery."""
+ discovery_topic = DEFAULT_PREFIX
+
+ assert mqtt_mock.async_subscribe.called
+ call_args = mqtt_mock.async_subscribe.mock_calls[0][1]
+ assert call_args[0] == discovery_topic + "/#"
+ assert call_args[2] == 0
+
+
+async def test_valid_discovery_message(hass, mqtt_mock, caplog):
+ """Test discovery callback called."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+
+ with patch(
+ "homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
+ ) as mock_tasmota_has_entities:
+ await setup_tasmota_helper(hass)
+
+ async_fire_mqtt_message(
+ hass, f"{DEFAULT_PREFIX}/00000049A3BC/config", json.dumps(config)
+ )
+ await hass.async_block_till_done()
+ assert mock_tasmota_has_entities.called
+
+
+async def test_invalid_topic(hass, mqtt_mock):
+ """Test receiving discovery message on wrong topic."""
+ with patch(
+ "homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
+ ) as mock_tasmota_has_entities:
+ await setup_tasmota_helper(hass)
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/123456/configuration", "{}")
+ await hass.async_block_till_done()
+ assert not mock_tasmota_has_entities.called
+
+
+async def test_invalid_message(hass, mqtt_mock, caplog):
+ """Test receiving an invalid message."""
+ with patch(
+ "homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
+ ) as mock_tasmota_has_entities:
+ await setup_tasmota_helper(hass)
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/123456/config", "asd")
+ await hass.async_block_till_done()
+ assert "Invalid discovery message" in caplog.text
+ assert not mock_tasmota_has_entities.called
+
+
+async def test_invalid_mac(hass, mqtt_mock, caplog):
+ """Test topic is not matching device MAC."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+
+ with patch(
+ "homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
+ ) as mock_tasmota_has_entities:
+ await setup_tasmota_helper(hass)
+
+ async_fire_mqtt_message(
+ hass, f"{DEFAULT_PREFIX}/00000049A3BA/config", json.dumps(config)
+ )
+ await hass.async_block_till_done()
+ assert "MAC mismatch" in caplog.text
+ assert not mock_tasmota_has_entities.called
+
+
+async def test_correct_config_discovery(
+ hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
+):
+ """Test receiving valid discovery message."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ # Verify device and registry entries are created
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+ entity_entry = entity_reg.async_get("switch.test")
+ assert entity_entry is not None
+
+ state = hass.states.get("switch.test")
+ assert state is not None
+ assert state.name == "Test"
+
+ assert (mac, "switch", "relay", 0) in hass.data[ALREADY_DISCOVERED]
+
+
+async def test_device_discover(
+ hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
+):
+ """Test setting up a device."""
+ 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 and registry entries are created
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+ assert device_entry.manufacturer == "Tasmota"
+ assert device_entry.model == config["md"]
+ assert device_entry.name == config["dn"]
+ assert device_entry.sw_version == config["sw"]
+
+
+async def test_device_update(
+ hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
+):
+ """Test updating a device."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["md"] = "Model 1"
+ config["dn"] = "Name 1"
+ config["sw"] = "v1.2.3.4"
+ 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(), {("mac", mac)})
+ assert device_entry is not None
+
+ # Update device parameters
+ config["md"] = "Another model"
+ config["dn"] = "Another name"
+ config["sw"] = "v6.6.6"
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ # Verify device entry is updated
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+ assert device_entry.model == "Another model"
+ assert device_entry.name == "Another name"
+ assert device_entry.sw_version == "v6.6.6"
+
+
+@pytest.mark.no_fail_on_log_exception
+async def test_discovery_broken(hass, mqtt_mock, caplog, device_reg, setup_tasmota):
+ """Test handling of exception when creating discovered device."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ mac = config["mac"]
+ data = json.dumps(config)
+
+ # Trigger an exception when the entity is added
+ with patch(
+ "hatasmota.discovery.get_device_config_helper",
+ return_value=object(),
+ ):
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", data)
+ await hass.async_block_till_done()
+
+ # Verify device entry is not created
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is None
+ assert (
+ "Exception in async_discover_device when dispatching 'tasmota_discovery_device'"
+ in caplog.text
+ )
+
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", data)
+ await hass.async_block_till_done()
+
+ # Verify device entry is created
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+
+
+async def test_device_remove(
+ hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
+):
+ """Test removing a discovered device."""
+ 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(), {("mac", mac)})
+ assert device_entry is not None
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ "",
+ )
+ await hass.async_block_till_done()
+
+ # Verify device entry is removed
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is None
+
+
+async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_tasmota):
+ """Test removing a stale (undiscovered) device does not throw."""
+ mac = "00000049A3BC"
+
+ config_entry = hass.config_entries.async_entries("tasmota")[0]
+
+ # Create a device
+ device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", mac)},
+ )
+
+ # Verify device entry was created
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+
+ # Remove the device
+ device_reg.async_remove_device(device_entry.id)
+
+ # Verify device entry is removed
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is None
+
+
+async def test_device_rediscover(
+ hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
+):
+ """Test removing a device."""
+ 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_entry1 = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry1 is not None
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ "",
+ )
+ await hass.async_block_till_done()
+
+ # Verify device entry is removed
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is None
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ # Verify device entry is created, and id is reused
+ device_entry = device_reg.async_get_device(set(), {("mac", mac)})
+ assert device_entry is not None
+ assert device_entry1.id == device_entry.id
+
+
+async def test_entity_duplicate_discovery(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test entities are not duplicated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ 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}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ state_duplicate = hass.states.get("binary_sensor.beer1")
+
+ assert state is not None
+ assert state.name == "Test"
+ assert state_duplicate is None
+ assert (
+ f"Entity already added, sending update: switch ('{mac}', 'switch', 'relay', 0)"
+ in caplog.text
+ )
+
+
+async def test_entity_duplicate_removal(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test removing entity twice."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+ config["rl"][0] = 0
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
+ await hass.async_block_till_done()
+ assert f"Removing entity: switch ('{mac}', 'switch', 'relay', 0)" in caplog.text
+
+ caplog.clear()
+ async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
+ await hass.async_block_till_done()
+ assert "Removing entity: switch" not in caplog.text
diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py
new file mode 100644
index 00000000000000..9d5eb0c598585f
--- /dev/null
+++ b/tests/components/tasmota/test_switch.py
@@ -0,0 +1,178 @@
+"""The tests for the Tasmota switch platform."""
+import copy
+import json
+
+from homeassistant.components import switch
+from homeassistant.components.tasmota.const import DEFAULT_PREFIX
+from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON
+
+from .test_common import (
+ DEFAULT_CONFIG,
+ help_test_availability,
+ help_test_availability_discovery_update,
+ help_test_availability_when_connection_lost,
+ help_test_discovery_device_remove,
+ help_test_discovery_removal,
+ help_test_discovery_update_unchanged,
+ help_test_entity_id_update_discovery_update,
+ help_test_entity_id_update_subscriptions,
+)
+
+from tests.async_mock import patch
+from tests.common import async_fire_mqtt_message
+from tests.components.switch import common
+
+
+async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.test")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
+
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_ON
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}')
+
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_OFF
+
+
+async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
+ """Test the sending MQTT commands."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_OFF
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+
+ # Turn the switch on and verify MQTT message is sent
+ await common.async_turn_on(hass, "switch.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/POWER1", "ON", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Tasmota is not optimistic, the state should still be off
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_OFF
+
+ # Turn the switch off and verify MQTT message is sent
+ await common.async_turn_off(hass, "switch.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/POWER1", "OFF", 0, False
+ )
+
+ state = hass.states.get("switch.test")
+ assert state.state == STATE_OFF
+
+
+async def test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, setup_tasmota
+):
+ """Test availability after MQTT disconnection."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ await help_test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, config
+ )
+
+
+async def test_availability(hass, mqtt_mock, setup_tasmota):
+ """Test availability."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ await help_test_availability(hass, mqtt_mock, switch.DOMAIN, config)
+
+
+async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test availability discovery update."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ await help_test_availability_discovery_update(
+ hass, mqtt_mock, switch.DOMAIN, config
+ )
+
+
+async def test_discovery_removal_switch(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test removal of discovered switch."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG)
+ config1["rl"][0] = 1
+ config2 = copy.deepcopy(DEFAULT_CONFIG)
+ config2["rl"][0] = 0
+
+ await help_test_discovery_removal(
+ hass, mqtt_mock, caplog, switch.DOMAIN, config1, config2
+ )
+
+
+async def test_discovery_update_unchanged_switch(
+ hass, mqtt_mock, caplog, setup_tasmota
+):
+ """Test update of discovered switch."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ with patch(
+ "homeassistant.components.tasmota.switch.TasmotaSwitch.discovery_update"
+ ) as discovery_update:
+ await help_test_discovery_update_unchanged(
+ hass, mqtt_mock, caplog, switch.DOMAIN, config, discovery_update
+ )
+
+
+async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota):
+ """Test device registry remove."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ unique_id = f"{DEFAULT_CONFIG['mac']}_switch_relay_0"
+ await help_test_discovery_device_remove(
+ hass, mqtt_mock, switch.DOMAIN, unique_id, config
+ )
+
+
+async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT subscriptions are managed when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ await help_test_entity_id_update_subscriptions(
+ hass, mqtt_mock, switch.DOMAIN, config
+ )
+
+
+async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT discovery update when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 1
+ await help_test_entity_id_update_discovery_update(
+ hass, mqtt_mock, switch.DOMAIN, config
+ )
diff --git a/tests/components/teksavvy/__init__.py b/tests/components/teksavvy/__init__.py
deleted file mode 100644
index 8c8a0fc82cafd3..00000000000000
--- a/tests/components/teksavvy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the teksavvy component."""
diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py
deleted file mode 100644
index b0de95d72d17b2..00000000000000
--- a/tests/components/teksavvy/test_sensor.py
+++ /dev/null
@@ -1,199 +0,0 @@
-"""Tests for the TekSavvy sensor platform."""
-from homeassistant.bootstrap import async_setup_component
-from homeassistant.components.teksavvy.sensor import TekSavvyData
-from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, PERCENTAGE
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-
-
-async def test_capped_setup(hass, aioclient_mock):
- """Test the default setup."""
- config = {
- "platform": "teksavvy",
- "api_key": "NOTAKEY",
- "total_bandwidth": 400,
- "monitored_variables": [
- "usage",
- "usage_gb",
- "limit",
- "onpeak_download",
- "onpeak_upload",
- "onpeak_total",
- "offpeak_download",
- "offpeak_upload",
- "offpeak_total",
- "onpeak_remaining",
- ],
- }
-
- result = (
- '{"odata.metadata":"http://api.teksavvy.com/web/Usage/$metadata'
- '#UsageSummaryRecords","value":[{'
- '"StartDate":"2018-01-01T00:00:00",'
- '"EndDate":"2018-01-31T00:00:00",'
- '"OID":"999999","IsCurrent":true,'
- '"OnPeakDownload":226.75,'
- '"OnPeakUpload":8.82,'
- '"OffPeakDownload":36.24,"OffPeakUpload":1.58'
- "}]}"
- )
- aioclient_mock.get(
- "https://api.teksavvy.com/"
- "web/Usage/UsageSummaryRecords?"
- "$filter=IsCurrent%20eq%20true",
- text=result,
- )
-
- await async_setup_component(hass, "sensor", {"sensor": config})
- await hass.async_block_till_done()
-
- state = hass.states.get("sensor.teksavvy_data_limit")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "400"
-
- state = hass.states.get("sensor.teksavvy_off_peak_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "36.24"
-
- state = hass.states.get("sensor.teksavvy_off_peak_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "1.58"
-
- state = hass.states.get("sensor.teksavvy_off_peak_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "37.82"
-
- state = hass.states.get("sensor.teksavvy_on_peak_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "226.75"
-
- state = hass.states.get("sensor.teksavvy_on_peak_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "8.82"
-
- state = hass.states.get("sensor.teksavvy_on_peak_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "235.57"
-
- state = hass.states.get("sensor.teksavvy_usage_ratio")
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
- assert state.state == "56.69"
-
- state = hass.states.get("sensor.teksavvy_usage")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "226.75"
-
- state = hass.states.get("sensor.teksavvy_remaining")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "173.25"
-
-
-async def test_unlimited_setup(hass, aioclient_mock):
- """Test the default setup."""
- config = {
- "platform": "teksavvy",
- "api_key": "NOTAKEY",
- "total_bandwidth": 0,
- "monitored_variables": [
- "usage",
- "usage_gb",
- "limit",
- "onpeak_download",
- "onpeak_upload",
- "onpeak_total",
- "offpeak_download",
- "offpeak_upload",
- "offpeak_total",
- "onpeak_remaining",
- ],
- }
-
- result = (
- '{"odata.metadata":"http://api.teksavvy.com/web/Usage/$metadata'
- '#UsageSummaryRecords","value":[{'
- '"StartDate":"2018-01-01T00:00:00",'
- '"EndDate":"2018-01-31T00:00:00",'
- '"OID":"999999","IsCurrent":true,'
- '"OnPeakDownload":226.75,'
- '"OnPeakUpload":8.82,'
- '"OffPeakDownload":36.24,"OffPeakUpload":1.58'
- "}]}"
- )
- aioclient_mock.get(
- "https://api.teksavvy.com/"
- "web/Usage/UsageSummaryRecords?"
- "$filter=IsCurrent%20eq%20true",
- text=result,
- )
-
- await async_setup_component(hass, "sensor", {"sensor": config})
- await hass.async_block_till_done()
-
- state = hass.states.get("sensor.teksavvy_data_limit")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "inf"
-
- state = hass.states.get("sensor.teksavvy_off_peak_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "36.24"
-
- state = hass.states.get("sensor.teksavvy_off_peak_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "1.58"
-
- state = hass.states.get("sensor.teksavvy_off_peak_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "37.82"
-
- state = hass.states.get("sensor.teksavvy_on_peak_download")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "226.75"
-
- state = hass.states.get("sensor.teksavvy_on_peak_upload")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "8.82"
-
- state = hass.states.get("sensor.teksavvy_on_peak_total")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "235.57"
-
- state = hass.states.get("sensor.teksavvy_usage")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "226.75"
-
- state = hass.states.get("sensor.teksavvy_usage_ratio")
- assert state.attributes.get("unit_of_measurement") == PERCENTAGE
- assert state.state == "0"
-
- state = hass.states.get("sensor.teksavvy_remaining")
- assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES
- assert state.state == "inf"
-
-
-async def test_bad_return_code(hass, aioclient_mock):
- """Test handling a return code that isn't HTTP OK."""
- aioclient_mock.get(
- "https://api.teksavvy.com/"
- "web/Usage/UsageSummaryRecords?"
- "$filter=IsCurrent%20eq%20true",
- status=HTTP_NOT_FOUND,
- )
-
- tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400)
-
- result = await tsd.async_update()
- assert result is False
-
-
-async def test_bad_json_decode(hass, aioclient_mock):
- """Test decoding invalid json result."""
- aioclient_mock.get(
- "https://api.teksavvy.com/"
- "web/Usage/UsageSummaryRecords?"
- "$filter=IsCurrent%20eq%20true",
- text="this is not json",
- )
-
- tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400)
-
- result = await tsd.async_update()
- assert result is False
diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py
index 5deb540782c05d..7202d8a3a1b3ae 100644
--- a/tests/components/template/test_cover.py
+++ b/tests/components/template/test_cover.py
@@ -115,6 +115,7 @@ async def test_template_state_boolean(hass, calls):
async def test_template_position(hass, calls):
"""Test the position_template attribute."""
+ hass.states.async_set("cover.test", STATE_OPEN)
with assert_setup_component(1, "cover"):
assert await setup.async_setup_component(
hass,
@@ -1120,3 +1121,48 @@ async def test_state_gets_lowercased(hass):
hass.states.async_set("binary_sensor.garage_door_sensor", "on")
await hass.async_block_till_done()
assert hass.states.get("cover.garage_door").state == STATE_CLOSED
+
+
+async def test_self_referencing_icon_with_no_template_is_not_a_loop(hass, caplog):
+ """Test a self referencing icon with no value template is not a loop."""
+
+ icon_template_str = """{% if is_state('cover.office', 'open') %}
+ mdi:window-shutter-open
+ {% else %}
+ mdi:window-shutter
+ {% endif %}"""
+
+ await setup.async_setup_component(
+ hass,
+ "cover",
+ {
+ "cover": {
+ "platform": "template",
+ "covers": {
+ "office": {
+ "icon_template": icon_template_str,
+ "open_cover": {
+ "service": "switch.turn_on",
+ "entity_id": "switch.office_blinds_up",
+ },
+ "close_cover": {
+ "service": "switch.turn_on",
+ "entity_id": "switch.office_blinds_down",
+ },
+ "stop_cover": {
+ "service": "switch.turn_on",
+ "entity_id": "switch.office_blinds_up",
+ },
+ },
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ assert "Template loop detected" not in caplog.text
diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py
index 2d14ec97574f90..1c932c5af30fa6 100644
--- a/tests/components/template/test_init.py
+++ b/tests/components/template/test_init.py
@@ -1,10 +1,14 @@
"""The test for the Template sensor platform."""
+from datetime import timedelta
from os import path
from unittest.mock import patch
from homeassistant import config
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from tests.common import async_fire_time_changed
async def test_reloadable(hass):
@@ -253,5 +257,50 @@ async def test_reloadable_multiple_platforms(hass):
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
+async def test_reload_sensors_that_reference_other_template_sensors(hass):
+ """Test that we can reload sensor that reference other template sensors."""
+
+ await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": DOMAIN,
+ "sensors": {
+ "state": {"value_template": "{{ 1 }}"},
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ yaml_path = path.join(
+ _get_fixtures_base_path(),
+ "fixtures",
+ "template/ref_configuration.yaml",
+ )
+ with patch.object(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()) == 3
+ await hass.async_block_till_done()
+
+ next_time = dt_util.utcnow() + timedelta(seconds=1.2)
+ with patch(
+ "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
+ ):
+ async_fire_time_changed(hass, next_time)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.test1").state == "3"
+ assert hass.states.get("sensor.test2").state == "1"
+ assert hass.states.get("sensor.test3").state == "2"
+
+
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))
diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py
index 439f154b4afa83..26cb35a0388cd7 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -1,9 +1,12 @@
"""The test for the Template sensor platform."""
from asyncio import Event
-from unittest.mock import patch
+from datetime import timedelta
from homeassistant.bootstrap import async_from_config_dict
+from homeassistant.components import sensor
from homeassistant.const import (
+ ATTR_ENTITY_PICTURE,
+ ATTR_ICON,
EVENT_COMPONENT_LOADED,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
@@ -12,439 +15,393 @@
)
from homeassistant.core import CoreState, callback
from homeassistant.helpers.template import Template
-from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component
+from homeassistant.setup import ATTR_COMPONENT, async_setup_component
import homeassistant.util.dt as dt_util
-from tests.common import assert_setup_component, get_test_home_assistant
+from tests.async_mock import patch
+from tests.common import assert_setup_component, async_fire_time_changed
-class TestTemplateSensor:
- """Test the Template sensor."""
+async def test_template(hass):
+ """Test template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "It {{ states.sensor.test_state.state }}."
+ }
+ },
+ }
+ },
+ )
- hass = None
- # pylint: disable=invalid-name
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.state == "It ."
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.state == "It Works."
- def test_template(self):
- """Test template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "It {{ states.sensor.test_state.state }}."
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.state == "It ."
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.state == "It Works."
-
- def test_icon_template(self):
- """Test icon template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.test_state.state }}",
- "icon_template": "{% if states.sensor.test_state.state == "
- "'Works' %}"
- "mdi:check"
- "{% endif %}",
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes.get("icon") == ""
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["icon"] == "mdi:check"
-
- def test_entity_picture_template(self):
- """Test entity_picture template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.test_state.state }}",
- "entity_picture_template": "{% if states.sensor.test_state.state == "
- "'Works' %}"
- "/local/sensor.png"
- "{% endif %}",
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes.get("entity_picture") == ""
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["entity_picture"] == "/local/sensor.png"
-
- def test_friendly_name_template(self):
- """Test friendly_name template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.test_state.state }}",
- "friendly_name_template": "It {{ states.sensor.test_state.state }}.",
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes.get("friendly_name") == "It ."
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["friendly_name"] == "It Works."
-
- def test_friendly_name_template_with_unknown_state(self):
- """Test friendly_name template with an unknown value_template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.fourohfour.state }}",
- "friendly_name_template": "It {{ states.sensor.test_state.state }}.",
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["friendly_name"] == "It ."
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["friendly_name"] == "It Works."
-
- def test_attribute_templates(self):
- """Test attribute_templates template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.test_state.state }}",
- "attribute_templates": {
- "test_attribute": "It {{ states.sensor.test_state.state }}."
- },
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes.get("test_attribute") == "It ."
-
- self.hass.states.set("sensor.test_state", "Works")
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.attributes["test_attribute"] == "It Works."
-
- def test_template_syntax_error(self):
- """Test templating syntax error."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{% if rubbish %}"
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
- assert self.hass.states.all() == []
-
- def test_template_attribute_missing(self):
- """Test missing attribute template."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "It {{ states.sensor.test_state"
- ".attributes.missing }}."
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test_template_sensor")
- assert state.state == STATE_UNAVAILABLE
-
- def test_invalid_name_does_not_create(self):
- """Test invalid name."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test INVALID sensor": {
- "value_template": "{{ states.sensor.test_state.state }}"
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- assert self.hass.states.all() == []
-
- def test_invalid_sensor_does_not_create(self):
- """Test invalid sensor."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {"test_template_sensor": "invalid"},
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
-
- assert self.hass.states.all() == []
-
- def test_no_sensors_does_not_create(self):
- """Test no sensors."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass, "sensor", {"sensor": {"platform": "template"}}
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- assert self.hass.states.all() == []
-
- def test_missing_template_does_not_create(self):
- """Test missing template."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "not_value_template": "{{ states.sensor.test_state.state }}"
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- assert self.hass.states.all() == []
-
- def test_setup_invalid_device_class(self):
- """Test setup with invalid device_class."""
- with assert_setup_component(0):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test": {
- "value_template": "{{ states.sensor.test_sensor.state }}",
- "device_class": "foobarnotreal",
- }
- },
- }
- },
- )
-
- def test_setup_valid_device_class(self):
- """Test setup with valid device_class."""
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test1": {
- "value_template": "{{ states.sensor.test_sensor.state }}",
- "device_class": "temperature",
- },
- "test2": {
- "value_template": "{{ states.sensor.test_sensor.state }}"
+
+async def test_icon_template(hass):
+ """Test icon template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}",
+ "icon_template": "{% if states.sensor.test_state.state == "
+ "'Works' %}"
+ "mdi:check"
+ "{% endif %}",
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes.get("icon") == ""
+
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["icon"] == "mdi:check"
+
+
+async def test_entity_picture_template(hass):
+ """Test entity_picture template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}",
+ "entity_picture_template": "{% if states.sensor.test_state.state == "
+ "'Works' %}"
+ "/local/sensor.png"
+ "{% endif %}",
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes.get("entity_picture") == ""
+
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["entity_picture"] == "/local/sensor.png"
+
+
+async def test_friendly_name_template(hass):
+ """Test friendly_name template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}",
+ "friendly_name_template": "It {{ states.sensor.test_state.state }}.",
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes.get("friendly_name") == "It ."
+
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["friendly_name"] == "It Works."
+
+
+async def test_friendly_name_template_with_unknown_state(hass):
+ """Test friendly_name template with an unknown value_template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.fourohfour.state }}",
+ "friendly_name_template": "It {{ states.sensor.test_state.state }}.",
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["friendly_name"] == "It ."
+
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["friendly_name"] == "It Works."
+
+
+async def test_attribute_templates(hass):
+ """Test attribute_templates template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}",
+ "attribute_templates": {
+ "test_attribute": "It {{ states.sensor.test_state.state }}."
},
- },
- }
- },
- )
- self.hass.block_till_done()
-
- state = self.hass.states.get("sensor.test1")
- assert state.attributes["device_class"] == "temperature"
- state = self.hass.states.get("sensor.test2")
- assert "device_class" not in state.attributes
-
- def test_available_template_with_entities(self):
- """Test availability tempalates with values from other entities."""
-
- with assert_setup_component(1):
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.test_state.state }}",
- "availability_template": "{{ is_state('availability_boolean.state', 'on') }}",
- }
- },
- }
- },
- )
+ }
+ },
+ }
+ },
+ )
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes.get("test_attribute") == "It ."
+
+ hass.states.async_set("sensor.test_state", "Works")
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.attributes["test_attribute"] == "It Works."
- # When template returns true..
- self.hass.states.set("availability_boolean.state", STATE_ON)
- self.hass.block_till_done()
- # Device State should not be unavailable
- assert (
- self.hass.states.get("sensor.test_template_sensor").state
- != STATE_UNAVAILABLE
+async def test_template_syntax_error(hass):
+ """Test templating syntax error."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {"value_template": "{% if rubbish %}"}
+ },
+ }
+ },
)
- # When Availability template returns false
- self.hass.states.set("availability_boolean.state", STATE_OFF)
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+ assert hass.states.async_all() == []
- # device state should be unavailable
- assert (
- self.hass.states.get("sensor.test_template_sensor").state
- == STATE_UNAVAILABLE
+
+async def test_template_attribute_missing(hass):
+ """Test missing attribute template."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "It {{ states.sensor.test_state"
+ ".attributes.missing }}."
+ }
+ },
+ }
+ },
)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state.state == STATE_UNAVAILABLE
+
+
+async def test_invalid_name_does_not_create(hass):
+ """Test invalid name."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test INVALID sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}"
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_invalid_sensor_does_not_create(hass):
+ """Test invalid sensor."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {"test_template_sensor": "invalid"},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+
+ assert hass.states.async_all() == []
+
+
+async def test_no_sensors_does_not_create(hass):
+ """Test no sensors."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass, sensor.DOMAIN, {"sensor": {"platform": "template"}}
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_missing_template_does_not_create(hass):
+ """Test missing template."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "not_value_template": "{{ states.sensor.test_state.state }}"
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_setup_invalid_device_class(hass):
+ """Test setup with invalid device_class."""
+ with assert_setup_component(0, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "value_template": "{{ states.sensor.test_sensor.state }}",
+ "device_class": "foobarnotreal",
+ }
+ },
+ }
+ },
+ )
+
+
+async def test_setup_valid_device_class(hass):
+ """Test setup with valid device_class."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test1": {
+ "value_template": "{{ states.sensor.test_sensor.state }}",
+ "device_class": "temperature",
+ },
+ "test2": {
+ "value_template": "{{ states.sensor.test_sensor.state }}"
+ },
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test1")
+ assert state.attributes["device_class"] == "temperature"
+ state = hass.states.get("sensor.test2")
+ assert "device_class" not in state.attributes
+
async def test_creating_sensor_loads_group(hass):
"""Test setting up template sensor loads group component first."""
@@ -489,10 +446,10 @@ async def set_after_dep_event(event):
async def test_available_template_with_entities(hass):
"""Test availability tempalates with values from other entities."""
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
- with assert_setup_component(1, "sensor"):
+ with assert_setup_component(1, sensor.DOMAIN):
assert await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -531,7 +488,7 @@ async def test_invalid_attribute_template(hass, caplog):
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -562,7 +519,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -592,7 +549,7 @@ async def test_no_template_match_all(hass, caplog):
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -674,7 +631,7 @@ async def test_unique_id(hass):
"""Test unique_id option only creates one sensor per id."""
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -709,7 +666,7 @@ async def test_sun_renders_once_per_sensor(hass):
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
@@ -763,21 +720,51 @@ def _record_async_render(self, *args, **kwargs):
async def test_self_referencing_sensor_loop(hass, caplog):
"""Test a self referencing sensor does not loop forever."""
- template_str = """
-{% for state in states -%}
- {{ state.last_updated }}
-{%- endfor %}
-"""
+ await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
+ },
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert "Template loop detected" in caplog.text
+
+ state = hass.states.get("sensor.test")
+ assert int(state.state) == 2
+ await hass.async_block_till_done()
+ assert int(state.state) == 2
+
+
+async def test_self_referencing_sensor_with_icon_loop(hass, caplog):
+ """Test a self referencing sensor loops forever with a valid self referencing icon."""
await async_setup_component(
hass,
- "sensor",
+ sensor.DOMAIN,
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
- "value_template": template_str,
+ "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
+ "icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}",
},
},
}
@@ -790,15 +777,172 @@ async def test_self_referencing_sensor_loop(hass, caplog):
assert len(hass.states.async_all()) == 1
- value = hass.states.get("sensor.test").state
await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert "Template loop detected" in caplog.text
+
+ state = hass.states.get("sensor.test")
+ assert int(state.state) == 3
+ assert state.attributes[ATTR_ICON] == "mdi:greater"
+
+ await hass.async_block_till_done()
+ assert int(state.state) == 3
+
+
+async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog):
+ """Test a self referencing sensor loop forevers with a valid self referencing icon."""
+
+ await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
+ "icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}",
+ "entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}",
+ },
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert "Template loop detected" in caplog.text
+
+ state = hass.states.get("sensor.test")
+ assert int(state.state) == 4
+ assert state.attributes[ATTR_ICON] == "mdi:less"
+ assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic"
+
+ await hass.async_block_till_done()
+ assert int(state.state) == 4
+
- value2 = hass.states.get("sensor.test").state
- assert value2 == value
+async def test_self_referencing_entity_picture_loop(hass, caplog):
+ """Test a self referencing sensor does not loop forever with a looping self referencing entity picture."""
+
+ await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "value_template": "{{ 1 }}",
+ "entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}",
+ },
+ },
+ }
+ },
+ )
await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
- value3 = hass.states.get("sensor.test").state
- assert value3 == value2
+ next_time = dt_util.utcnow() + timedelta(seconds=1.2)
+ with patch(
+ "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
+ ):
+ async_fire_time_changed(hass, next_time)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert "Template loop detected" in caplog.text
+
+ state = hass.states.get("sensor.test")
+ assert int(state.state) == 1
+ assert state.attributes[ATTR_ENTITY_PICTURE] == "2"
+
+ await hass.async_block_till_done()
+ assert int(state.state) == 1
+
+
+async def test_self_referencing_icon_with_no_loop(hass, caplog):
+ """Test a self referencing icon that does not loop."""
+
+ hass.states.async_set("sensor.heartworm_high_80", 10)
+ hass.states.async_set("sensor.heartworm_low_57", 10)
+ hass.states.async_set("sensor.heartworm_avg_64", 10)
+ hass.states.async_set("sensor.heartworm_avg_57", 10)
+
+ value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %}
+ extreme
+ {% elif (states.sensor.heartworm_avg_64.state|int >= 30) %}
+ high
+ {% elif (states.sensor.heartworm_avg_64.state|int >= 14) %}
+ moderate
+ {% elif (states.sensor.heartworm_avg_64.state|int >= 5) %}
+ slight
+ {% elif (states.sensor.heartworm_avg_57.state|int >= 5) %}
+ marginal
+ {% elif (states.sensor.heartworm_avg_57.state|int < 5) %}
+ none
+ {% endif %}"""
+
+ icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %}
+ mdi:hazard-lights
+ {% elif is_state('sensor.heartworm_risk',"high") %}
+ mdi:triangle-outline
+ {% elif is_state('sensor.heartworm_risk',"moderate") %}
+ mdi:alert-circle-outline
+ {% elif is_state('sensor.heartworm_risk',"slight") %}
+ mdi:exclamation
+ {% elif is_state('sensor.heartworm_risk',"marginal") %}
+ mdi:heart
+ {% elif is_state('sensor.heartworm_risk',"none") %}
+ mdi:snowflake
+ {% endif %}"""
+
+ await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "sensors": {
+ "heartworm_risk": {
+ "value_template": value_template_str,
+ "icon_template": icon_template_str,
+ },
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 5
+
+ hass.states.async_set("sensor.heartworm_high_80", 10)
+
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert "Template loop detected" not in caplog.text
+
+ state = hass.states.get("sensor.heartworm_risk")
+ assert state.state == "extreme"
+ assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
+
+ await hass.async_block_till_done()
+ assert state.state == "extreme"
+ assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
+ assert "Template loop detected" not in caplog.text
diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py
index 6dab2569e59129..9bf7ef99956f23 100644
--- a/tests/components/template/test_switch.py
+++ b/tests/components/template/test_switch.py
@@ -3,7 +3,15 @@
import pytest
from homeassistant import setup
-from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+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,
+ STATE_UNAVAILABLE,
+)
from homeassistant.core import CoreState, State
from homeassistant.setup import async_setup_component
@@ -13,7 +21,6 @@
mock_component,
mock_restore_cache,
)
-from tests.components.switch import common
@pytest.fixture
@@ -418,8 +425,12 @@ async def test_on_action(hass, calls):
state = hass.states.get("switch.test_template_switch")
assert state.state == STATE_OFF
- await common.async_turn_on(hass, "switch.test_template_switch")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test_template_switch"},
+ blocking=True,
+ )
assert len(calls) == 1
@@ -454,8 +465,12 @@ async def test_on_action_optimistic(hass, calls):
state = hass.states.get("switch.test_template_switch")
assert state.state == STATE_OFF
- await common.async_turn_on(hass, "switch.test_template_switch")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test_template_switch"},
+ blocking=True,
+ )
state = hass.states.get("switch.test_template_switch")
assert len(calls) == 1
@@ -494,8 +509,12 @@ async def test_off_action(hass, calls):
state = hass.states.get("switch.test_template_switch")
assert state.state == STATE_ON
- await common.async_turn_off(hass, "switch.test_template_switch")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test_template_switch"},
+ blocking=True,
+ )
assert len(calls) == 1
@@ -530,8 +549,12 @@ async def test_off_action_optimistic(hass, calls):
state = hass.states.get("switch.test_template_switch")
assert state.state == STATE_ON
- await common.async_turn_off(hass, "switch.test_template_switch")
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test_template_switch"},
+ blocking=True,
+ )
state = hass.states.get("switch.test_template_switch")
assert len(calls) == 1
diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py
index 300173fdadf98d..4bda4dc23cab63 100644
--- a/tests/components/template/test_trigger.py
+++ b/tests/components/template/test_trigger.py
@@ -6,6 +6,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.template import trigger as template_trigger
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context, callback
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -16,7 +17,6 @@
async_mock_service,
mock_component,
)
-from tests.components.automation import common
@pytest.fixture
@@ -52,8 +52,12 @@ async def test_if_fires_on_change_bool(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.states.async_set("test.entity", "planet")
await hass.async_block_till_done()
@@ -698,8 +702,12 @@ async def test_if_not_fires_when_turned_off_with_for(hass, calls):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4))
await hass.async_block_till_done()
assert len(calls) == 0
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
assert len(calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6))
await hass.async_block_till_done()
diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py
index a59daee9ac6672..8b75fd904b99c7 100644
--- a/tests/components/tesla/test_config_flow.py
+++ b/tests/components/tesla/test_config_flow.py
@@ -70,7 +70,7 @@ async def test_form_invalid_auth(hass):
)
assert result2["type"] == "form"
- assert result2["errors"] == {"base": "invalid_credentials"}
+ assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass):
@@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass):
)
assert result2["type"] == "form"
- assert result2["errors"] == {"base": "connection_error"}
+ assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_repeat_identifier(hass):
@@ -110,7 +110,7 @@ async def test_form_repeat_identifier(hass):
)
assert result2["type"] == "form"
- assert result2["errors"] == {CONF_USERNAME: "identifier_exists"}
+ assert result2["errors"] == {CONF_USERNAME: "already_configured"}
async def test_import(hass):
diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py
index 3eb6299b3bedf7..66ab9e6cac1ff0 100644
--- a/tests/components/threshold/test_binary_sensor.py
+++ b/tests/components/threshold/test_binary_sensor.py
@@ -1,384 +1,385 @@
"""The test for the threshold sensor platform."""
-import unittest
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant
-
-class TestThresholdSensor(unittest.TestCase):
- """Test the threshold sensor."""
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_sensor_upper(self):
- """Test if source is above threshold."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "upper": "15",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_upper(hass):
+ """Test if source is above threshold."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "upper": "15",
+ "entity_id": "sensor.test_monitored",
}
+ }
+
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "binary_sensor", config)
+ hass.states.async_set(
+ "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
+ )
+ await hass.async_block_till_done()
- self.hass.states.set(
- "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
- )
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "sensor.test_monitored" == state.attributes.get("entity_id")
+ assert 16 == state.attributes.get("sensor_value")
+ assert "above" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert 0.0 == state.attributes.get("hysteresis")
+ assert "upper" == state.attributes.get("type")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "upper" == state.attributes.get("type")
+ assert state.state == "on"
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 14)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 14)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert state.state == "off"
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 15)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 15)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert state.state == "off"
- assert state.state == "off"
- def test_sensor_lower(self):
- """Test if source is below threshold."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "lower": "15",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_lower(hass):
+ """Test if source is below threshold."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "lower": "15",
+ "entity_id": "sensor.test_monitored",
}
+ }
+
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "binary_sensor", config)
+ hass.states.async_set("sensor.test_monitored", 16)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 16)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "above" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert 0.0 == state.attributes.get("hysteresis")
+ assert "lower" == state.attributes.get("type")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "lower" == state.attributes.get("type")
+ assert state.state == "off"
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 14)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 14)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert state.state == "on"
- assert state.state == "on"
- def test_sensor_hysteresis(self):
- """Test if source is above threshold using hysteresis."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "upper": "15",
- "hysteresis": "2.5",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_hysteresis(hass):
+ """Test if source is above threshold using hysteresis."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "upper": "15",
+ "hysteresis": "2.5",
+ "entity_id": "sensor.test_monitored",
}
+ }
- assert setup_component(self.hass, "binary_sensor", config)
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 20)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 20)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 2.5 == state.attributes.get("hysteresis")
- assert "upper" == state.attributes.get("type")
+ assert "above" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert 2.5 == state.attributes.get("hysteresis")
+ assert "upper" == state.attributes.get("type")
- assert state.state == "on"
+ assert state.state == "on"
- self.hass.states.set("sensor.test_monitored", 13)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 13)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert state.state == "on"
+ assert state.state == "on"
- self.hass.states.set("sensor.test_monitored", 12)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 12)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert state.state == "off"
+ assert state.state == "off"
- self.hass.states.set("sensor.test_monitored", 17)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 17)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert state.state == "off"
+ assert state.state == "off"
- self.hass.states.set("sensor.test_monitored", 18)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 18)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert state.state == "on"
+ assert state.state == "on"
- def test_sensor_in_range_no_hysteresis(self):
- """Test if source is within the range."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "lower": "10",
- "upper": "20",
- "entity_id": "sensor.test_monitored",
- }
+
+async def test_sensor_in_range_no_hysteresis(hass):
+ """Test if source is within the range."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "lower": "10",
+ "upper": "20",
+ "entity_id": "sensor.test_monitored",
}
+ }
+
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "binary_sensor", config)
+ hass.states.async_set(
+ "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
+ )
+ await hass.async_block_till_done()
- self.hass.states.set(
- "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
- )
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert state.attributes.get("entity_id") == "sensor.test_monitored"
+ assert 16 == state.attributes.get("sensor_value")
+ assert "in_range" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert 0.0 == state.attributes.get("hysteresis")
+ assert "range" == state.attributes.get("type")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "range" == state.attributes.get("type")
+ assert state.state == "on"
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 9)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 9)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "below" == state.attributes.get("position")
+ assert state.state == "off"
- assert "below" == state.attributes.get("position")
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 21)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 21)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "above" == state.attributes.get("position")
+ assert state.state == "off"
- assert "above" == state.attributes.get("position")
- assert state.state == "off"
- def test_sensor_in_range_with_hysteresis(self):
- """Test if source is within the range."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "lower": "10",
- "upper": "20",
- "hysteresis": "2",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_in_range_with_hysteresis(hass):
+ """Test if source is within the range."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "lower": "10",
+ "upper": "20",
+ "hysteresis": "2",
+ "entity_id": "sensor.test_monitored",
}
+ }
+
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "binary_sensor", config)
+ hass.states.async_set(
+ "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
+ )
+ await hass.async_block_till_done()
- self.hass.states.set(
- "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
- )
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "sensor.test_monitored" == state.attributes.get("entity_id")
+ assert 16 == state.attributes.get("sensor_value")
+ assert "in_range" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert float(config["binary_sensor"]["hysteresis"]) == state.attributes.get(
+ "hysteresis"
+ )
+ assert "range" == state.attributes.get("type")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert float(config["binary_sensor"]["hysteresis"]) == state.attributes.get(
- "hysteresis"
- )
- assert "range" == state.attributes.get("type")
+ assert state.state == "on"
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 8)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 8)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "in_range" == state.attributes.get("position")
+ assert state.state == "on"
- assert "in_range" == state.attributes.get("position")
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 7)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 7)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "below" == state.attributes.get("position")
+ assert state.state == "off"
- assert "below" == state.attributes.get("position")
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 12)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 12)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "below" == state.attributes.get("position")
+ assert state.state == "off"
- assert "below" == state.attributes.get("position")
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 13)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 13)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "in_range" == state.attributes.get("position")
+ assert state.state == "on"
- assert "in_range" == state.attributes.get("position")
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 22)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 22)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "in_range" == state.attributes.get("position")
+ assert state.state == "on"
- assert "in_range" == state.attributes.get("position")
- assert state.state == "on"
+ hass.states.async_set("sensor.test_monitored", 23)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 23)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "above" == state.attributes.get("position")
+ assert state.state == "off"
- assert "above" == state.attributes.get("position")
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 18)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 18)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "above" == state.attributes.get("position")
+ assert state.state == "off"
- assert "above" == state.attributes.get("position")
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", 17)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 17)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "in_range" == state.attributes.get("position")
+ assert state.state == "on"
- assert "in_range" == state.attributes.get("position")
- assert state.state == "on"
- def test_sensor_in_range_unknown_state(self):
- """Test if source is within the range."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "lower": "10",
- "upper": "20",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_in_range_unknown_state(hass):
+ """Test if source is within the range."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "lower": "10",
+ "upper": "20",
+ "entity_id": "sensor.test_monitored",
}
+ }
- assert setup_component(self.hass, "binary_sensor", config)
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- self.hass.states.set(
- "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
- )
- self.hass.block_till_done()
+ hass.states.async_set(
+ "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "range" == state.attributes.get("type")
+ assert "sensor.test_monitored" == state.attributes.get("entity_id")
+ assert 16 == state.attributes.get("sensor_value")
+ assert "in_range" == state.attributes.get("position")
+ assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert 0.0 == state.attributes.get("hysteresis")
+ assert "range" == state.attributes.get("type")
- assert state.state == "on"
+ assert state.state == "on"
- self.hass.states.set("sensor.test_monitored", STATE_UNKNOWN)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert "unknown" == state.attributes.get("position")
- assert state.state == "off"
+ assert "unknown" == state.attributes.get("position")
+ assert state.state == "off"
- def test_sensor_lower_zero_threshold(self):
- """Test if a lower threshold of zero is set."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "lower": "0",
- "entity_id": "sensor.test_monitored",
- }
+
+async def test_sensor_lower_zero_threshold(hass):
+ """Test if a lower threshold of zero is set."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "lower": "0",
+ "entity_id": "sensor.test_monitored",
}
+ }
+
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- assert setup_component(self.hass, "binary_sensor", config)
+ hass.states.async_set("sensor.test_monitored", 16)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", 16)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert "lower" == state.attributes.get("type")
+ assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert "lower" == state.attributes.get("type")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert state.state == "off"
- assert state.state == "off"
+ hass.states.async_set("sensor.test_monitored", -3)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", -3)
- self.hass.block_till_done()
+ state = hass.states.get("binary_sensor.threshold")
- state = self.hass.states.get("binary_sensor.threshold")
+ assert state.state == "on"
- assert state.state == "on"
- def test_sensor_upper_zero_threshold(self):
- """Test if an upper threshold of zero is set."""
- config = {
- "binary_sensor": {
- "platform": "threshold",
- "upper": "0",
- "entity_id": "sensor.test_monitored",
- }
+async def test_sensor_upper_zero_threshold(hass):
+ """Test if an upper threshold of zero is set."""
+ config = {
+ "binary_sensor": {
+ "platform": "threshold",
+ "upper": "0",
+ "entity_id": "sensor.test_monitored",
}
+ }
- assert setup_component(self.hass, "binary_sensor", config)
+ assert await async_setup_component(hass, "binary_sensor", config)
+ await hass.async_block_till_done()
- self.hass.states.set("sensor.test_monitored", -10)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", -10)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert "upper" == state.attributes.get("type")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert "upper" == state.attributes.get("type")
+ assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert state.state == "off"
+ assert state.state == "off"
- self.hass.states.set("sensor.test_monitored", 2)
- self.hass.block_till_done()
+ hass.states.async_set("sensor.test_monitored", 2)
+ await hass.async_block_till_done()
- state = self.hass.states.get("binary_sensor.threshold")
+ state = hass.states.get("binary_sensor.threshold")
- assert state.state == "on"
+ assert state.state == "on"
diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py
index a1812d34a57711..b9d4799dc2f974 100644
--- a/tests/components/tile/test_config_flow.py
+++ b/tests/components/tile/test_config_flow.py
@@ -44,7 +44,7 @@ async def test_invalid_credentials(hass):
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "invalid_credentials"}
+ assert result["errors"] == {"base": "invalid_auth"}
async def test_step_import(hass):
diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py
index 80cfe7a81f8b22..fd8cdcc5116d32 100644
--- a/tests/components/totalconnect/test_config_flow.py
+++ b/tests/components/totalconnect/test_config_flow.py
@@ -100,4 +100,4 @@ async def test_login_failed(hass):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "login"}
+ assert result["errors"] == {"base": "invalid_auth"}
diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py
index 0820e71ae3b20b..0b57ab59913102 100644
--- a/tests/components/transmission/test_config_flow.py
+++ b/tests/components/transmission/test_config_flow.py
@@ -264,8 +264,8 @@ async def test_error_on_wrong_credentials(hass, auth_error):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {
- CONF_USERNAME: "wrong_credentials",
- CONF_PASSWORD: "wrong_credentials",
+ CONF_USERNAME: "invalid_auth",
+ CONF_PASSWORD: "invalid_auth",
}
diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py
index d2aa8fb93874f5..a6f9c7dfd7f7cb 100644
--- a/tests/components/transport_nsw/test_sensor.py
+++ b/tests/components/transport_nsw/test_sensor.py
@@ -1,10 +1,7 @@
"""The tests for the Transport NSW (AU) sensor platform."""
-import unittest
-
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from tests.async_mock import patch
-from tests.common import get_test_home_assistant
VALID_CONFIG = {
"sensor": {
@@ -31,24 +28,16 @@ def get_departuresMock(_stop_id, route, destination, api_key):
return data
-class TestRMVtransportSensor(unittest.TestCase):
- """Test the TransportNSW sensor."""
-
- def setUp(self):
- """Set up things to run when tests begin."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- @patch("TransportNSW.TransportNSW.get_departures", side_effect=get_departuresMock)
- def test_transportnsw_config(self, mock_get_departures):
- """Test minimal TransportNSW configuration."""
- assert setup_component(self.hass, "sensor", VALID_CONFIG)
- self.hass.block_till_done()
- state = self.hass.states.get("sensor.next_bus")
- assert state.state == "16"
- assert state.attributes["stop_id"] == "209516"
- assert state.attributes["route"] == "199"
- assert state.attributes["delay"] == 6
- assert state.attributes["real_time"] == "y"
- assert state.attributes["destination"] == "Palm Beach"
- assert state.attributes["mode"] == "Bus"
+@patch("TransportNSW.TransportNSW.get_departures", side_effect=get_departuresMock)
+async def test_transportnsw_config(mocked_get_departures, hass):
+ """Test minimal TransportNSW configuration."""
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.next_bus")
+ assert state.state == "16"
+ assert state.attributes["stop_id"] == "209516"
+ assert state.attributes["route"] == "199"
+ assert state.attributes["delay"] == 6
+ assert state.attributes["real_time"] == "y"
+ assert state.attributes["destination"] == "Palm Beach"
+ assert state.attributes["mode"] == "Bus"
diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py
index 2ce298640dedce..81154a368ff5e9 100644
--- a/tests/components/tuya/test_config_flow.py
+++ b/tests/components/tuya/test_config_flow.py
@@ -120,14 +120,14 @@ async def test_abort_on_invalid_credentials(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "auth_failed"}
+ assert result["errors"] == {"base": "invalid_auth"}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "auth_failed"
+ assert result["reason"] == "invalid_auth"
async def test_abort_on_connection_error(hass, tuya):
@@ -139,11 +139,11 @@ async def test_abort_on_connection_error(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "conn_error"
+ assert result["reason"] == "cannot_connect"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "conn_error"
+ assert result["reason"] == "cannot_connect"
diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py
index 7dd19e755f310f..d176c6b1ee4c26 100644
--- a/tests/components/twentemilieu/test_config_flow.py
+++ b/tests/components/twentemilieu/test_config_flow.py
@@ -9,7 +9,7 @@
CONF_POST_CODE,
DOMAIN,
)
-from homeassistant.const import CONF_ID
+from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON
from tests.common import MockConfigEntry
@@ -51,7 +51,7 @@ async def test_invalid_address(hass, aioclient_mock):
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": []},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.TwenteMilieuFlowHandler()
@@ -72,7 +72,7 @@ async def test_address_already_set_up(hass, aioclient_mock):
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.TwenteMilieuFlowHandler()
@@ -88,7 +88,7 @@ async def test_full_flow_implementation(hass, aioclient_mock):
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.TwenteMilieuFlowHandler()
diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py
index 81cfa7ae8ae759..ff6cf3d1142148 100644
--- a/tests/components/uk_transport/test_sensor.py
+++ b/tests/components/uk_transport/test_sensor.py
@@ -1,6 +1,5 @@
"""The tests for the uk_transport platform."""
import re
-import unittest
import requests_mock
@@ -16,11 +15,11 @@
CONF_API_APP_KEY,
UkTransportSensor,
)
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from homeassistant.util.dt import now
from tests.async_mock import patch
-from tests.common import get_test_home_assistant, load_fixture
+from tests.common import load_fixture
BUS_ATCOCODE = "340000368SHE"
BUS_DIRECTION = "Wantage"
@@ -28,73 +27,63 @@
TRAIN_DESTINATION_NAME = "WAT"
VALID_CONFIG = {
- "platform": "uk_transport",
- CONF_API_APP_ID: "foo",
- CONF_API_APP_KEY: "ebcd1234",
- "queries": [
- {"mode": "bus", "origin": BUS_ATCOCODE, "destination": BUS_DIRECTION},
- {
- "mode": "train",
- "origin": TRAIN_STATION_CODE,
- "destination": TRAIN_DESTINATION_NAME,
- },
- ],
+ "sensor": {
+ "platform": "uk_transport",
+ CONF_API_APP_ID: "foo",
+ CONF_API_APP_KEY: "ebcd1234",
+ "queries": [
+ {"mode": "bus", "origin": BUS_ATCOCODE, "destination": BUS_DIRECTION},
+ {
+ "mode": "train",
+ "origin": TRAIN_STATION_CODE,
+ "destination": TRAIN_DESTINATION_NAME,
+ },
+ ],
+ }
}
-class TestUkTransportSensor(unittest.TestCase):
- """Test the uk_transport platform."""
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.config = VALID_CONFIG
- self.addCleanup(self.hass.stop)
-
- @requests_mock.Mocker()
- def test_bus(self, mock_req):
- """Test for operational uk_transport sensor with proper attributes."""
- with requests_mock.Mocker() as mock_req:
- uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*")
- mock_req.get(uri, text=load_fixture("uk_transport_bus.json"))
- assert setup_component(self.hass, "sensor", {"sensor": self.config})
- self.hass.block_till_done()
-
- bus_state = self.hass.states.get("sensor.next_bus_to_wantage")
-
- assert type(bus_state.state) == str
- assert bus_state.name == f"Next bus to {BUS_DIRECTION}"
- assert bus_state.attributes.get(ATTR_ATCOCODE) == BUS_ATCOCODE
- assert bus_state.attributes.get(ATTR_LOCALITY) == "Harwell Campus"
- assert bus_state.attributes.get(ATTR_STOP_NAME) == "Bus Station"
- assert len(bus_state.attributes.get(ATTR_NEXT_BUSES)) == 2
-
- direction_re = re.compile(BUS_DIRECTION)
- for bus in bus_state.attributes.get(ATTR_NEXT_BUSES):
- print(bus["direction"], direction_re.match(bus["direction"]))
- assert direction_re.search(bus["direction"]) is not None
-
- @requests_mock.Mocker()
- def test_train(self, mock_req):
- """Test for operational uk_transport sensor with proper attributes."""
- with requests_mock.Mocker() as mock_req, patch(
- "homeassistant.util.dt.now", return_value=now().replace(hour=13)
- ):
- uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*")
- mock_req.get(uri, text=load_fixture("uk_transport_train.json"))
- assert setup_component(self.hass, "sensor", {"sensor": self.config})
- self.hass.block_till_done()
-
- train_state = self.hass.states.get("sensor.next_train_to_WAT")
-
- assert type(train_state.state) == str
- assert train_state.name == f"Next train to {TRAIN_DESTINATION_NAME}"
- assert train_state.attributes.get(ATTR_STATION_CODE) == TRAIN_STATION_CODE
- assert train_state.attributes.get(ATTR_CALLING_AT) == TRAIN_DESTINATION_NAME
- assert len(train_state.attributes.get(ATTR_NEXT_TRAINS)) == 25
-
- assert (
- train_state.attributes.get(ATTR_NEXT_TRAINS)[0]["destination_name"]
- == "London Waterloo"
- )
- assert train_state.attributes.get(ATTR_NEXT_TRAINS)[0]["estimated"] == "06:13"
+async def test_bus(hass):
+ """Test for operational uk_transport sensor with proper attributes."""
+ with requests_mock.Mocker() as mock_req:
+ uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*")
+ mock_req.get(uri, text=load_fixture("uk_transport_bus.json"))
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
+
+ bus_state = hass.states.get("sensor.next_bus_to_wantage")
+ assert None is not bus_state
+ assert f"Next bus to {BUS_DIRECTION}" == bus_state.name
+ assert BUS_ATCOCODE == bus_state.attributes[ATTR_ATCOCODE]
+ assert "Harwell Campus" == bus_state.attributes[ATTR_LOCALITY]
+ assert "Bus Station" == bus_state.attributes[ATTR_STOP_NAME]
+ assert 2 == len(bus_state.attributes.get(ATTR_NEXT_BUSES))
+
+ direction_re = re.compile(BUS_DIRECTION)
+ for bus in bus_state.attributes.get(ATTR_NEXT_BUSES):
+ assert None is not bus
+ assert None is not direction_re.search(bus["direction"])
+
+
+async def test_train(hass):
+ """Test for operational uk_transport sensor with proper attributes."""
+ with requests_mock.Mocker() as mock_req, patch(
+ "homeassistant.util.dt.now", return_value=now().replace(hour=13)
+ ):
+ uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*")
+ mock_req.get(uri, text=load_fixture("uk_transport_train.json"))
+ assert await async_setup_component(hass, "sensor", VALID_CONFIG)
+ await hass.async_block_till_done()
+
+ train_state = hass.states.get("sensor.next_train_to_WAT")
+ assert None is not train_state
+ assert f"Next train to {TRAIN_DESTINATION_NAME}" == train_state.name
+ assert TRAIN_STATION_CODE == train_state.attributes[ATTR_STATION_CODE]
+ assert TRAIN_DESTINATION_NAME == train_state.attributes[ATTR_CALLING_AT]
+ assert 25 == len(train_state.attributes.get(ATTR_NEXT_TRAINS))
+
+ assert (
+ "London Waterloo"
+ == train_state.attributes[ATTR_NEXT_TRAINS][0]["destination_name"]
+ )
+ assert "06:13" == train_state.attributes[ATTR_NEXT_TRAINS][0]["estimated"]
diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py
index a1af12dfb7657e..8b935d7744b3bf 100644
--- a/tests/components/unifi/test_config_flow.py
+++ b/tests/components/unifi/test_config_flow.py
@@ -4,6 +4,7 @@
from homeassistant import data_entry_flow
from homeassistant.components.unifi.const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -22,6 +23,7 @@
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
from .test_controller import setup_unifi_integration
@@ -93,7 +95,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery):
aioclient_mock.post(
"https://1.2.3.4:1234/api/login",
json={"data": "login successful", "meta": {"rc": "ok"}},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -102,7 +104,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery):
"data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
"meta": {"rc": "ok"},
},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -144,7 +146,7 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock):
aioclient_mock.post(
"https://1.2.3.4:1234/api/login",
json={"data": "login successful", "meta": {"rc": "ok"}},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -156,7 +158,7 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock):
],
"meta": {"rc": "ok"},
},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -195,7 +197,7 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock):
aioclient_mock.post(
"https://1.2.3.4:1234/api/login",
json={"data": "login successful", "meta": {"rc": "ok"}},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
@@ -204,7 +206,7 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock):
"data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
"meta": {"rc": "ok"},
},
- headers={"content-type": "application/json"},
+ headers={"content-type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_configure(
@@ -341,7 +343,11 @@ async def test_advanced_option_flow(hass):
assert result["step_id"] == "statistics_sensors"
result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input={CONF_ALLOW_BANDWIDTH_SENSORS: True}
+ result["flow_id"],
+ user_input={
+ CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -355,6 +361,7 @@ async def test_advanced_option_flow(hass):
CONF_POE_CLIENTS: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
}
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 5600f4543369c1..5fee4a85f9a872 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -13,6 +13,7 @@
CONF_CONTROLLER,
CONF_SITE_ID,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
+ DEFAULT_ALLOW_UPTIME_SENSORS,
DEFAULT_DETECTION_TIME,
DEFAULT_TRACK_CLIENTS,
DEFAULT_TRACK_DEVICES,
@@ -49,6 +50,7 @@
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
+ "uptime": 1562600160,
}
CONTROLLER_DATA = {
@@ -175,6 +177,7 @@ async def test_controller_setup(hass):
assert controller.site_role == SITES[controller.site_name]["role"]
assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS
+ assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS
assert isinstance(controller.option_block_clients, list)
assert controller.option_track_clients == DEFAULT_TRACK_CLIENTS
assert controller.option_track_devices == DEFAULT_TRACK_DEVICES
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index 2d5fbe96e0fb13..690b9d77899367 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -8,10 +8,12 @@
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.unifi.const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
DOMAIN as UNIFI_DOMAIN,
)
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
from .test_controller import setup_unifi_integration
@@ -29,6 +31,7 @@
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
+ "uptime": 1600094505,
},
{
"hostname": "Wireless client hostname",
@@ -42,6 +45,7 @@
"sw_port": 2,
"rx_bytes": 1234000000,
"tx_bytes": 5678000000,
+ "uptime": 1600094505,
},
]
@@ -61,7 +65,10 @@ async def test_no_clients(hass):
"""Test the update_clients function when no clients are found."""
controller = await setup_unifi_integration(
hass,
- options={CONF_ALLOW_BANDWIDTH_SENSORS: True},
+ options={
+ CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
+ },
)
assert len(controller.mock_requests) == 4
@@ -74,6 +81,7 @@ async def test_sensors(hass):
hass,
options={
CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
},
@@ -81,7 +89,7 @@ async def test_sensors(hass):
)
assert len(controller.mock_requests) == 4
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
assert wired_client_rx.state == "1234.0"
@@ -89,16 +97,23 @@ async def test_sensors(hass):
wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
assert wired_client_tx.state == "5678.0"
+ wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
+ assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00"
+
wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
assert wireless_client_rx.state == "1234.0"
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx.state == "5678.0"
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime.state == "2020-09-14T14:41:45+00:00"
+
clients = deepcopy(CLIENTS)
clients[0]["is_wired"] = False
clients[1]["rx_bytes"] = 2345000000
clients[1]["tx_bytes"] = 6789000000
+ clients[1]["uptime"] = 1600180860
event = {"meta": {"message": MESSAGE_CLIENT}, "data": clients}
controller.api.message_handler(event)
@@ -110,9 +125,15 @@ async def test_sensors(hass):
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx.state == "6789.0"
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00"
+
hass.config_entries.async_update_entry(
controller.config_entry,
- options={CONF_ALLOW_BANDWIDTH_SENSORS: False},
+ options={
+ CONF_ALLOW_BANDWIDTH_SENSORS: False,
+ CONF_ALLOW_UPTIME_SENSORS: False,
+ },
)
await hass.async_block_till_done()
@@ -122,9 +143,18 @@ async def test_sensors(hass):
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx is None
+ wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
+ assert wired_client_uptime is None
+
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime is None
+
hass.config_entries.async_update_entry(
controller.config_entry,
- options={CONF_ALLOW_BANDWIDTH_SENSORS: True},
+ options={
+ CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
+ },
)
await hass.async_block_till_done()
@@ -134,15 +164,42 @@ async def test_sensors(hass):
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx.state == "6789.0"
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00"
+
+ wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
+ assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00"
+
+ # Try to add the sensors again, using a signal
+ clients_connected = set()
+ devices_connected = set()
+
+ clients_connected.add(clients[0]["mac"])
+ clients_connected.add(clients[1]["mac"])
+
+ async_dispatcher_send(
+ hass,
+ controller.signal_update,
+ clients_connected,
+ devices_connected,
+ )
+
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
+
async def test_remove_sensors(hass):
"""Test the remove_items function with some clients."""
controller = await setup_unifi_integration(
hass,
- options={CONF_ALLOW_BANDWIDTH_SENSORS: True},
+ options={
+ CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: True,
+ },
clients_response=CLIENTS,
)
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
@@ -150,11 +207,17 @@ async def test_remove_sensors(hass):
wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
assert wired_client_tx is not None
+ wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
+ assert wired_client_uptime is not None
+
wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
assert wireless_client_rx is not None
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx is not None
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime is not None
+
controller.api.websocket._data = {
"meta": {"message": MESSAGE_CLIENT_REMOVED},
"data": [CLIENTS[0]],
@@ -162,7 +225,7 @@ async def test_remove_sensors(hass):
controller.api.session_handler(SIGNAL_DATA)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
@@ -170,7 +233,13 @@ async def test_remove_sensors(hass):
wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
assert wired_client_tx is None
+ wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
+ assert wired_client_uptime is None
+
wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
assert wireless_client_rx is not None
wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
assert wireless_client_tx is not None
+
+ wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
+ assert wireless_client_uptime is not None
diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py
index 38949c32ebbbea..76a397496ad0b7 100644
--- a/tests/components/universal/test_media_player.py
+++ b/tests/components/universal/test_media_player.py
@@ -20,6 +20,7 @@
STATE_PLAYING,
STATE_UNKNOWN,
)
+from homeassistant.core import Context, callback
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
@@ -812,10 +813,18 @@ async def test_master_state_with_template(hass):
await hass.async_block_till_done()
hass.states.get("media_player.tv").state == STATE_ON
- hass.states.async_set("input_boolean.test", STATE_ON)
+ events = []
+
+ hass.helpers.event.async_track_state_change_event(
+ "media_player.tv", callback(lambda event: events.append(event))
+ )
+
+ context = Context()
+ hass.states.async_set("input_boolean.test", STATE_ON, context=context)
await hass.async_block_till_done()
hass.states.get("media_player.tv").state == STATE_OFF
+ assert events[0].context == context
async def test_reload(hass):
diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py
index c1440caedfd1ae..f0b44a4bad82a2 100644
--- a/tests/components/upb/test_config_flow.py
+++ b/tests/components/upb/test_config_flow.py
@@ -107,7 +107,7 @@ async def test_form_user_with_already_configured(hass):
_ = await valid_tcp_flow(hass)
result2 = await valid_tcp_flow(hass)
assert result2["type"] == "abort"
- assert result2["reason"] == "address_already_configured"
+ assert result2["reason"] == "already_configured"
await hass.async_block_till_done()
diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py
index 98df710d8feb20..997811a835f5f8 100644
--- a/tests/components/upnp/test_config_flow.py
+++ b/tests/components/upnp/test_config_flow.py
@@ -98,10 +98,9 @@ async def test_flow_user(hass: HomeAssistantType):
"""Test config flow: discovered + configured through user."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
- usn = f"{mock_device.udn}::{mock_device.device_type}"
discovery_infos = [
{
- DISCOVERY_USN: usn,
+ DISCOVERY_USN: mock_device.unique_id,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
@@ -121,7 +120,7 @@ async def test_flow_user(hass: HomeAssistantType):
# Confirmed via step user.
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={"usn": usn},
+ user_input={"usn": mock_device.unique_id},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -132,14 +131,13 @@ async def test_flow_user(hass: HomeAssistantType):
}
-async def test_flow_config(hass: HomeAssistantType):
+async def test_flow_import(hass: HomeAssistantType):
"""Test config flow: discovered + configured through configuration.yaml."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
- usn = f"{mock_device.udn}::{mock_device.device_type}"
discovery_infos = [
{
- DISCOVERY_USN: usn,
+ DISCOVERY_USN: mock_device.unique_id,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
@@ -162,6 +160,66 @@ async def test_flow_config(hass: HomeAssistantType):
}
+async def test_flow_import_duplicate(hass: HomeAssistantType):
+ """Test config flow: discovered, but already configured."""
+ udn = "uuid:device_1"
+ mock_device = MockDevice(udn)
+ discovery_infos = [
+ {
+ DISCOVERY_USN: mock_device.unique_id,
+ DISCOVERY_ST: mock_device.device_type,
+ DISCOVERY_UDN: mock_device.udn,
+ DISCOVERY_LOCATION: "dummy",
+ }
+ ]
+
+ # Existing entry.
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONFIG_ENTRY_UDN: mock_device.udn,
+ CONFIG_ENTRY_ST: mock_device.device_type,
+ },
+ options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch.object(
+ Device, "async_create_device", AsyncMock(return_value=mock_device)
+ ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
+ # Discovered via step import.
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_flow_import_incomplete(hass: HomeAssistantType):
+ """Test config flow: incomplete discovery, configured through configuration.yaml."""
+ udn = "uuid:device_1"
+ mock_device = MockDevice(udn)
+ discovery_infos = [
+ {
+ DISCOVERY_ST: mock_device.device_type,
+ DISCOVERY_UDN: mock_device.udn,
+ DISCOVERY_LOCATION: "dummy",
+ }
+ ]
+
+ with patch.object(
+ Device, "async_create_device", AsyncMock(return_value=mock_device)
+ ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
+ # Discovered via step import.
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "incomplete_discovery"
+
+
async def test_options_flow(hass: HomeAssistantType):
"""Test options flow."""
# Set up config entry.
diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py
index 7116077177a8aa..f946ce352a5286 100644
--- a/tests/components/utility_meter/test_init.py
+++ b/tests/components/utility_meter/test_init.py
@@ -12,6 +12,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_UNIT_OF_MEASUREMENT,
ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_START,
)
@@ -41,7 +42,9 @@ async def test_services(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
- hass.states.async_set(entity_id, 1, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR})
+ hass.states.async_set(
+ entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
+ )
await hass.async_block_till_done()
now = dt_util.utcnow() + timedelta(seconds=10)
@@ -49,7 +52,7 @@ async def test_services(hass):
hass.states.async_set(
entity_id,
3,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -70,7 +73,7 @@ async def test_services(hass):
hass.states.async_set(
entity_id,
4,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -91,7 +94,7 @@ async def test_services(hass):
hass.states.async_set(
entity_id,
5,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py
index c1613c53a204a9..1fff168b7488e9 100644
--- a/tests/components/utility_meter/test_sensor.py
+++ b/tests/components/utility_meter/test_sensor.py
@@ -13,6 +13,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_UNIT_OF_MEASUREMENT,
ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_START,
)
@@ -52,7 +53,9 @@ async def test_state(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
- hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR})
+ hass.states.async_set(
+ entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
+ )
await hass.async_block_till_done()
now = dt_util.utcnow() + timedelta(seconds=10)
@@ -60,7 +63,7 @@ async def test_state(hass):
hass.states.async_set(
entity_id,
3,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -91,7 +94,7 @@ async def test_state(hass):
hass.states.async_set(
entity_id,
6,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -145,7 +148,9 @@ async def test_net_consumption(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
- hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR})
+ hass.states.async_set(
+ entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
+ )
await hass.async_block_till_done()
now = dt_util.utcnow() + timedelta(seconds=10)
@@ -153,7 +158,7 @@ async def test_net_consumption(hass):
hass.states.async_set(
entity_id,
1,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -178,7 +183,9 @@ async def test_non_net_consumption(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
- hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR})
+ hass.states.async_set(
+ entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
+ )
await hass.async_block_till_done()
now = dt_util.utcnow() + timedelta(seconds=10)
@@ -186,7 +193,7 @@ async def test_non_net_consumption(hass):
hass.states.async_set(
entity_id,
1,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -224,7 +231,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True):
with alter_time(now):
async_fire_time_changed(hass, now)
hass.states.async_set(
- entity_id, 1, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}
+ entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
await hass.async_block_till_done()
@@ -234,7 +241,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True):
hass.states.async_set(
entity_id,
3,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -246,7 +253,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True):
hass.states.async_set(
entity_id,
6,
- {"unit_of_measurement": ENERGY_KILO_WATT_HOUR},
+ {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
@@ -288,6 +295,23 @@ async def test_self_reset_monthly(hass, legacy_patchable_time):
)
+async def test_self_reset_bimonthly(hass, legacy_patchable_time):
+ """Test bimonthly reset of meter occurs on even months."""
+ await _test_self_reset(
+ hass, gen_config("bimonthly"), "2017-12-31T23:59:00.000000+00:00"
+ )
+
+
+async def test_self_no_reset_bimonthly(hass, legacy_patchable_time):
+ """Test bimonthly reset of meter does not occur on odd months."""
+ await _test_self_reset(
+ hass,
+ gen_config("bimonthly"),
+ "2018-01-01T23:59:00.000000+00:00",
+ expect_reset=False,
+ )
+
+
async def test_self_reset_quarterly(hass, legacy_patchable_time):
"""Test quarterly reset of meter."""
await _test_self_reset(
diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py
index 3bccacc0a94faa..0a7bc4489c77fc 100644
--- a/tests/components/velbus/test_config_flow.py
+++ b/tests/components/velbus/test_config_flow.py
@@ -65,13 +65,13 @@ async def test_user_fail(hass, controller_assert):
{CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_PORT: "connection_failed"}
+ assert result["errors"] == {CONF_PORT: "cannot_connect"}
result = await flow.async_step_user(
{CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_PORT: "connection_failed"}
+ assert result["errors"] == {CONF_PORT: "cannot_connect"}
async def test_import(hass, controller):
@@ -94,10 +94,10 @@ async def test_abort_if_already_setup(hass):
{CONF_PORT: PORT_TCP, CONF_NAME: "velbus import test"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "port_exists"
+ assert result["reason"] == "already_configured"
result = await flow.async_step_user(
{CONF_PORT: PORT_TCP, CONF_NAME: "velbus import test"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"port": "port_exists"}
+ assert result["errors"] == {"port": "already_configured"}
diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py
index 31e7c706ec9c73..29c6a0e868330a 100644
--- a/tests/components/vera/common.py
+++ b/tests/components/vera/common.py
@@ -1,10 +1,15 @@
"""Common code for tests."""
-
+from enum import Enum
from typing import Callable, Dict, NamedTuple, Tuple
import pyvera as pv
-from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN
+from homeassistant import config_entries
+from homeassistant.components.vera.const import (
+ CONF_CONTROLLER,
+ CONF_LEGACY_UNIQUE_ID,
+ DOMAIN,
+)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -24,7 +29,15 @@ class ControllerData(NamedTuple):
class ComponentData(NamedTuple):
"""Test data about the vera component."""
- controller_data: ControllerData
+ controller_data: Tuple[ControllerData]
+
+
+class ConfigSource(Enum):
+ """Source of configuration."""
+
+ FILE = "file"
+ CONFIG_FLOW = "config_flow"
+ CONFIG_ENTRY = "config_entry"
class ControllerConfig(NamedTuple):
@@ -32,31 +45,34 @@ class ControllerConfig(NamedTuple):
config: Dict
options: Dict
- config_from_file: bool
+ config_source: ConfigSource
serial_number: str
devices: Tuple[pv.VeraDevice, ...]
scenes: Tuple[pv.VeraScene, ...]
setup_callback: SetupCallback
+ legacy_entity_unique_id: bool
def new_simple_controller_config(
config: dict = None,
options: dict = None,
- config_from_file=False,
+ config_source=ConfigSource.CONFIG_FLOW,
serial_number="1111",
devices: Tuple[pv.VeraDevice, ...] = (),
scenes: Tuple[pv.VeraScene, ...] = (),
setup_callback: SetupCallback = None,
+ legacy_entity_unique_id=False,
) -> ControllerConfig:
"""Create simple contorller config."""
return ControllerConfig(
config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"},
options=options,
- config_from_file=config_from_file,
+ config_source=config_source,
serial_number=serial_number,
devices=devices,
scenes=scenes,
setup_callback=setup_callback,
+ legacy_entity_unique_id=legacy_entity_unique_id,
)
@@ -68,14 +84,38 @@ def __init__(self, vera_controller_class_mock):
self.vera_controller_class_mock = vera_controller_class_mock
async def configure_component(
- self, hass: HomeAssistant, controller_config: ControllerConfig
+ self,
+ hass: HomeAssistant,
+ controller_config: ControllerConfig = None,
+ controller_configs: Tuple[ControllerConfig] = (),
) -> ComponentData:
+ """Configure the component with multiple specific mock data."""
+ configs = list(controller_configs)
+
+ if controller_config:
+ configs.append(controller_config)
+
+ return ComponentData(
+ controller_data=tuple(
+ [
+ await self._configure_component(hass, controller_config)
+ for controller_config in configs
+ ]
+ )
+ )
+
+ async def _configure_component(
+ self, hass: HomeAssistant, controller_config: ControllerConfig
+ ) -> ControllerData:
"""Configure the component with specific mock data."""
component_config = {
**(controller_config.config or {}),
**(controller_config.options or {}),
}
+ if controller_config.legacy_entity_unique_id:
+ component_config[CONF_LEGACY_UNIQUE_ID] = True
+
controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController
controller.base_url = component_config.get(CONF_CONTROLLER)
controller.register = MagicMock()
@@ -101,7 +141,7 @@ async def configure_component(
hass_config = {}
# Setup component through config file import.
- if controller_config.config_from_file:
+ if controller_config.config_source == ConfigSource.FILE:
hass_config[DOMAIN] = component_config
# Setup Home Assistant.
@@ -109,9 +149,21 @@ async def configure_component(
await hass.async_block_till_done()
# Setup component through config flow.
- if not controller_config.config_from_file:
+ if controller_config.config_source == ConfigSource.CONFIG_FLOW:
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data=component_config,
+ )
+ await hass.async_block_till_done()
+
+ # Setup component directly from config entry.
+ if controller_config.config_source == ConfigSource.CONFIG_ENTRY:
entry = MockConfigEntry(
- domain=DOMAIN, data=component_config, options={}, unique_id="12345"
+ domain=DOMAIN,
+ data=controller_config.config,
+ options=controller_config.options,
+ unique_id="12345",
)
entry.add_to_hass(hass)
@@ -124,8 +176,4 @@ async def configure_component(
else None
)
- return ComponentData(
- controller_data=ControllerData(
- controller=controller, update_callback=update_callback
- )
- )
+ return ControllerData(controller=controller, update_callback=update_callback)
diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py
index 4b0d41d9a1eb9b..a02c2ef163549f 100644
--- a/tests/components/vera/test_binary_sensor.py
+++ b/tests/components/vera/test_binary_sensor.py
@@ -14,7 +14,7 @@ async def test_binary_sensor(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device.device_id = 1
- vera_device.vera_device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.is_tripped = False
entity_id = "binary_sensor.dev1_1"
@@ -23,7 +23,7 @@ async def test_binary_sensor(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
vera_device.is_tripped = False
update_callback(vera_device)
diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py
index f11f3ea5a3bd70..370ecc18dcd68b 100644
--- a/tests/components/vera/test_climate.py
+++ b/tests/components/vera/test_climate.py
@@ -22,6 +22,7 @@ async def test_climate(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
@@ -34,7 +35,7 @@ async def test_climate(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == HVAC_MODE_OFF
@@ -131,6 +132,7 @@ async def test_climate_f(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
@@ -148,7 +150,7 @@ def setup_callback(controller: pv.VeraController) -> None:
devices=(vera_device,), setup_callback=setup_callback
),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
await hass.services.async_call(
"climate",
diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py
index 793e313125c4e0..dceac728e4d467 100644
--- a/tests/components/vera/test_config_flow.py
+++ b/tests/components/vera/test_config_flow.py
@@ -2,17 +2,13 @@
from requests.exceptions import RequestException
from homeassistant import config_entries, data_entry_flow
-from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN
+from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
from homeassistant.core import HomeAssistant
-from homeassistant.data_entry_flow import (
- RESULT_TYPE_ABORT,
- RESULT_TYPE_CREATE_ENTRY,
- RESULT_TYPE_FORM,
-)
+from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from tests.async_mock import MagicMock, patch
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, mock_registry
async def test_async_step_user_success(hass: HomeAssistant) -> None:
@@ -44,6 +40,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
CONF_SOURCE: config_entries.SOURCE_USER,
CONF_LIGHTS: [12, 13],
CONF_EXCLUDE: [14, 15],
+ CONF_LEGACY_UNIQUE_ID: False,
}
assert result["result"].unique_id == controller.serial_number
@@ -51,18 +48,6 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
assert entries
-async def test_async_step_user_already_configured(hass: HomeAssistant) -> None:
- """Test user step with entry already configured."""
- entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345")
- entry.add_to_hass(hass)
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
- assert result["type"] == RESULT_TYPE_ABORT
- assert result["reason"] == "already_configured"
-
-
async def test_async_step_import_success(hass: HomeAssistant) -> None:
"""Test import step success."""
with patch("pyvera.VeraController") as vera_controller_class_mock:
@@ -82,28 +67,40 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_CONTROLLER: "http://127.0.0.1:123",
CONF_SOURCE: config_entries.SOURCE_IMPORT,
+ CONF_LEGACY_UNIQUE_ID: False,
}
assert result["result"].unique_id == controller.serial_number
-async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None:
- """Test import step with entry already setup."""
- entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345")
- entry.add_to_hass(hass)
+async def test_async_step_import_success_with_legacy_unique_id(
+ hass: HomeAssistant,
+) -> None:
+ """Test import step success with legacy unique id."""
+ entity_registry = mock_registry(hass)
+ entity_registry.async_get_or_create(
+ domain="switch", platform=DOMAIN, unique_id="12"
+ )
with patch("pyvera.VeraController") as vera_controller_class_mock:
controller = MagicMock()
controller.refresh_data = MagicMock()
- controller.serial_number = "12345"
+ controller.serial_number = "serial_number_1"
vera_controller_class_mock.return_value = controller
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={CONF_CONTROLLER: "http://localhost:445"},
+ data={CONF_CONTROLLER: "http://127.0.0.1:123/"},
)
- assert result["type"] == RESULT_TYPE_ABORT
- assert result["reason"] == "already_configured"
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "http://127.0.0.1:123"
+ assert result["data"] == {
+ CONF_CONTROLLER: "http://127.0.0.1:123",
+ CONF_SOURCE: config_entries.SOURCE_IMPORT,
+ CONF_LEGACY_UNIQUE_ID: True,
+ }
+ assert result["result"].unique_id == controller.serial_number
async def test_async_step_finish_error(hass: HomeAssistant) -> None:
diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py
index 311c8013d86879..f3dc22637491e1 100644
--- a/tests/components/vera/test_cover.py
+++ b/tests/components/vera/test_cover.py
@@ -14,6 +14,7 @@ async def test_cover(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_CURTAIN
vera_device.is_closed = False
@@ -24,7 +25,7 @@ async def test_cover(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "closed"
assert hass.states.get(entity_id).attributes["current_position"] == 0
diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py
index 210037a2ca3c5c..b3f7b3249efa2a 100644
--- a/tests/components/vera/test_init.py
+++ b/tests/components/vera/test_init.py
@@ -12,10 +12,10 @@
from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED
from homeassistant.core import HomeAssistant
-from .common import ComponentFactory, new_simple_controller_config
+from .common import ComponentFactory, ConfigSource, new_simple_controller_config
from tests.async_mock import MagicMock
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, mock_registry
async def test_init(
@@ -24,7 +24,7 @@ async def test_init(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
- vera_device1.vera_device_id = 1
+ vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
@@ -33,7 +33,7 @@ async def test_init(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:111"},
- config_from_file=False,
+ config_source=ConfigSource.CONFIG_FLOW,
serial_number="first_serial",
devices=(vera_device1,),
),
@@ -41,8 +41,8 @@ async def test_init(
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry1 = entity_registry.async_get(entity1_id)
-
assert entry1
+ assert entry1.unique_id == "vera_first_serial_1"
async def test_init_from_file(
@@ -51,24 +51,80 @@ async def test_init_from_file(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
- vera_device1.vera_device_id = 1
+ vera_device1.vera_device_id = vera_device1.device_id
+ vera_device1.name = "first_dev"
+ vera_device1.is_tripped = False
+ entity1_id = "binary_sensor.first_dev_1"
+
+ await vera_component_factory.configure_component(
+ hass=hass,
+ controller_config=new_simple_controller_config(
+ config={CONF_CONTROLLER: "http://127.0.0.1:111"},
+ config_source=ConfigSource.FILE,
+ serial_number="first_serial",
+ devices=(vera_device1,),
+ ),
+ )
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry1 = entity_registry.async_get(entity1_id)
+ assert entry1
+ assert entry1.unique_id == "vera_first_serial_1"
+
+
+async def test_multiple_controllers_with_legacy_one(
+ hass: HomeAssistant, vera_component_factory: ComponentFactory
+) -> None:
+ """Test multiple controllers with one legacy controller."""
+ vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1.device_id = 1
+ vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
+ vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device2.device_id = 2
+ vera_device2.vera_device_id = vera_device2.device_id
+ vera_device2.name = "second_dev"
+ vera_device2.is_tripped = False
+ entity2_id = "binary_sensor.second_dev_2"
+
+ # Add existing entity registry entry from previous setup.
+ entity_registry = mock_registry(hass)
+ entity_registry.async_get_or_create(
+ domain="switch", platform=DOMAIN, unique_id="12"
+ )
+
await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:111"},
- config_from_file=True,
+ config_source=ConfigSource.FILE,
serial_number="first_serial",
devices=(vera_device1,),
),
)
+ await vera_component_factory.configure_component(
+ hass=hass,
+ controller_config=new_simple_controller_config(
+ config={CONF_CONTROLLER: "http://127.0.0.1:222"},
+ config_source=ConfigSource.CONFIG_FLOW,
+ serial_number="second_serial",
+ devices=(vera_device2,),
+ ),
+ )
+
entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
entry1 = entity_registry.async_get(entity1_id)
assert entry1
+ assert entry1.unique_id == "1"
+
+ entry2 = entity_registry.async_get(entity2_id)
+ assert entry2
+ assert entry2.unique_id == "vera_second_serial_2"
async def test_unload(
@@ -77,7 +133,7 @@ async def test_unload(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
- vera_device1.vera_device_id = 1
+ vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
@@ -145,6 +201,7 @@ async def test_exclude_and_light_ids(
vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device3.device_id = 3
+ vera_device3.vera_device_id = 3
vera_device3.name = "dev3"
vera_device3.category = pv.CATEGORY_SWITCH
vera_device3.is_switched_on = MagicMock(return_value=False)
@@ -152,6 +209,7 @@ async def test_exclude_and_light_ids(
vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device4.device_id = 4
+ vera_device4.vera_device_id = 4
vera_device4.name = "dev4"
vera_device4.category = pv.CATEGORY_SWITCH
vera_device4.is_switched_on = MagicMock(return_value=False)
@@ -160,6 +218,7 @@ async def test_exclude_and_light_ids(
component_data = await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(
+ config_source=ConfigSource.CONFIG_ENTRY,
devices=(vera_device1, vera_device2, vera_device3, vera_device4),
config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options},
),
@@ -167,12 +226,10 @@ async def test_exclude_and_light_ids(
# Assert the entries were setup correctly.
config_entry = next(iter(hass.config_entries.async_entries(DOMAIN)))
- assert config_entry.options == {
- CONF_LIGHTS: [4, 10, 12],
- CONF_EXCLUDE: [1],
- }
+ assert config_entry.options[CONF_LIGHTS] == [4, 10, 12]
+ assert config_entry.options[CONF_EXCLUDE] == [1]
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
update_callback(vera_device1)
update_callback(vera_device2)
diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py
index 99391d8d82a18f..72118e33a316e2 100644
--- a/tests/components/vera/test_light.py
+++ b/tests/components/vera/test_light.py
@@ -15,6 +15,7 @@ async def test_light(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_DIMMER
vera_device.is_switched_on = MagicMock(return_value=False)
@@ -27,7 +28,7 @@ async def test_light(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "off"
diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py
index 11af1f5a7b7f41..b3433b2bafbcd0 100644
--- a/tests/components/vera/test_lock.py
+++ b/tests/components/vera/test_lock.py
@@ -15,6 +15,7 @@ async def test_lock(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_LOCK
vera_device.is_locked = MagicMock(return_value=False)
@@ -24,7 +25,7 @@ async def test_lock(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == STATE_UNLOCKED
diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py
index 29ef338b9f13e5..6c80f27d8c85ed 100644
--- a/tests/components/vera/test_scene.py
+++ b/tests/components/vera/test_scene.py
@@ -14,6 +14,7 @@ async def test_scene(
"""Test function."""
vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene
vera_scene.scene_id = 1
+ vera_scene.vera_scene_id = vera_scene.scene_id
vera_scene.name = "dev1"
entity_id = "scene.dev1_1"
diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py
index 36730e8d6d29e9..3d6b11b0685fa8 100644
--- a/tests/components/vera/test_sensor.py
+++ b/tests/components/vera/test_sensor.py
@@ -3,7 +3,7 @@
import pyvera as pv
-from homeassistant.const import PERCENTAGE
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, LIGHT_LUX, PERCENTAGE
from homeassistant.core import HomeAssistant
from .common import ComponentFactory, new_simple_controller_config
@@ -23,6 +23,7 @@ async def run_sensor_test(
"""Test generic sensor."""
vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = category
setattr(vera_device, class_property, "33")
@@ -34,7 +35,7 @@ async def run_sensor_test(
devices=(vera_device,), setup_callback=setup_callback
),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
for (initial_value, state_value) in assert_states:
setattr(vera_device, class_property, initial_value)
@@ -43,7 +44,9 @@ async def run_sensor_test(
state = hass.states.get(entity_id)
assert state.state == state_value
if assert_unit_of_measurement:
- assert state.attributes["unit_of_measurement"] == assert_unit_of_measurement
+ assert (
+ state.attributes[ATTR_UNIT_OF_MEASUREMENT] == assert_unit_of_measurement
+ )
async def test_temperature_sensor_f(
@@ -87,7 +90,7 @@ async def test_light_sensor(
category=pv.CATEGORY_LIGHT_SENSOR,
class_property="light",
assert_states=(("12", "12"), ("13", "13")),
- assert_unit_of_measurement="lx",
+ assert_unit_of_measurement=LIGHT_LUX,
)
@@ -175,6 +178,7 @@ async def test_scene_controller_sensor(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SCENE_CONTROLLER
vera_device.get_last_scene_id = MagicMock(return_value="id0")
@@ -185,7 +189,7 @@ async def test_scene_controller_sensor(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
vera_device.get_last_scene_time.return_value = "1111"
update_callback(vera_device)
diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py
index 60e31add4bd0ce..42c74e4e843ea9 100644
--- a/tests/components/vera/test_switch.py
+++ b/tests/components/vera/test_switch.py
@@ -14,6 +14,7 @@ async def test_switch(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device.device_id = 1
+ vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SWITCH
vera_device.is_switched_on = MagicMock(return_value=False)
@@ -21,9 +22,11 @@ async def test_switch(
component_data = await vera_component_factory.configure_component(
hass=hass,
- controller_config=new_simple_controller_config(devices=(vera_device,)),
+ controller_config=new_simple_controller_config(
+ devices=(vera_device,), legacy_entity_unique_id=False
+ ),
)
- update_callback = component_data.controller_data.update_callback
+ update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "off"
diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py
index aedf94da4abfe4..c82307d351cb0e 100644
--- a/tests/components/vesync/test_config_flow.py
+++ b/tests/components/vesync/test_config_flow.py
@@ -17,7 +17,7 @@ async def test_abort_already_setup(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_setup"
+ assert result["reason"] == "single_instance_allowed"
async def test_invalid_login_error(hass):
@@ -29,7 +29,7 @@ async def test_invalid_login_error(hass):
result = await flow.async_step_user(user_input=test_dict)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "invalid_login"}
+ assert result["errors"] == {"base": "invalid_auth"}
async def test_config_flow_configuration_yaml(hass):
diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py
index 08e5da5c9e52ab..c8a9083bb1a7c3 100644
--- a/tests/components/vizio/conftest.py
+++ b/tests/components/vizio/conftest.py
@@ -22,7 +22,7 @@
MockStartPairingResponse,
)
-from tests.async_mock import patch
+from tests.async_mock import AsyncMock, patch
class MockInput:
@@ -53,7 +53,7 @@ def vizio_get_unique_id_fixture():
"""Mock get vizio unique ID."""
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id",
- return_value=UNIQUE_ID,
+ AsyncMock(return_value=UNIQUE_ID),
):
yield
@@ -83,7 +83,7 @@ def vizio_connect_fixture():
"""Mock valid vizio device and entry setup."""
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
- return_value=True,
+ AsyncMock(return_value=True),
):
yield
@@ -156,7 +156,7 @@ def vizio_cant_connect_fixture():
"""Mock vizio device can't connect with valid auth."""
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
- return_value=False,
+ AsyncMock(return_value=False),
):
yield
diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py
index 33764de8696123..cd6116625977e2 100644
--- a/tests/components/vizio/test_init.py
+++ b/tests/components/vizio/test_init.py
@@ -6,7 +6,7 @@
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
-from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID
+from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID
from tests.common import MockConfigEntry
@@ -24,12 +24,12 @@ async def test_setup_component(
assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1
-async def test_load_and_unload(
+async def test_tv_load_and_unload(
hass: HomeAssistantType,
vizio_connect: pytest.fixture,
vizio_update: pytest.fixture,
) -> None:
- """Test loading and unloading entry."""
+ """Test loading and unloading TV entry."""
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
)
@@ -43,3 +43,24 @@ async def test_load_and_unload(
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
assert DOMAIN not in hass.data
+
+
+async def test_speaker_load_and_unload(
+ hass: HomeAssistantType,
+ vizio_connect: pytest.fixture,
+ vizio_update: pytest.fixture,
+) -> None:
+ """Test loading and unloading speaker entry."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID
+ )
+ 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(MP_DOMAIN)) == 1
+ assert DOMAIN in hass.data
+
+ assert await config_entry.async_unload(hass)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
+ assert DOMAIN not in hass.data
diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py
index ccddb35ad0ace0..c32c4d0952383e 100644
--- a/tests/components/weather/test_weather.py
+++ b/tests/components/weather/test_weather.py
@@ -1,6 +1,4 @@
"""The tests for the Weather component."""
-import unittest
-
from homeassistant.components import weather
from homeassistant.components.weather import (
ATTR_FORECAST,
@@ -17,68 +15,58 @@
ATTR_WEATHER_WIND_BEARING,
ATTR_WEATHER_WIND_SPEED,
)
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM
-from tests.common import get_test_home_assistant
-
-class TestWeather(unittest.TestCase):
- """Test the Weather component."""
+async def test_attributes(hass):
+ """Test weather attributes."""
+ assert await async_setup_component(
+ hass, weather.DOMAIN, {"weather": {"platform": "demo"}}
+ )
+ hass.config.units = METRIC_SYSTEM
+ await hass.async_block_till_done()
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.hass.config.units = METRIC_SYSTEM
- assert setup_component(
- self.hass, weather.DOMAIN, {"weather": {"platform": "demo"}}
- )
- self.hass.block_till_done()
- self.addCleanup(self.tear_down_cleanup)
+ state = hass.states.get("weather.demo_weather_south")
+ assert state is not None
- def tear_down_cleanup(self):
- """Stop down everything that was started."""
- self.hass.stop()
+ assert state.state == "sunny"
- def test_attributes(self):
- """Test weather attributes."""
- state = self.hass.states.get("weather.demo_weather_south")
- assert state is not None
+ data = state.attributes
+ assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6
+ assert data.get(ATTR_WEATHER_HUMIDITY) == 92
+ assert data.get(ATTR_WEATHER_PRESSURE) == 1099
+ assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5
+ assert data.get(ATTR_WEATHER_WIND_BEARING) is None
+ assert data.get(ATTR_WEATHER_OZONE) is None
+ assert data.get(ATTR_WEATHER_ATTRIBUTION) == "Powered by Home Assistant"
+ assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy"
+ assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1
+ assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 60
+ assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22
+ assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15
+ assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == "fog"
+ assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) == 0.2
+ assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21
+ assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12
+ assert (
+ data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100
+ )
+ assert len(data.get(ATTR_FORECAST)) == 7
- assert state.state == "sunny"
- data = state.attributes
- assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6
- assert data.get(ATTR_WEATHER_HUMIDITY) == 92
- assert data.get(ATTR_WEATHER_PRESSURE) == 1099
- assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5
- assert data.get(ATTR_WEATHER_WIND_BEARING) is None
- assert data.get(ATTR_WEATHER_OZONE) is None
- assert data.get(ATTR_WEATHER_ATTRIBUTION) == "Powered by Home Assistant"
- assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy"
- assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1
- assert (
- data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY)
- == 60
- )
- assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22
- assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15
- assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == "fog"
- assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) == 0.2
- assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21
- assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12
- assert (
- data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY)
- == 100
- )
- assert len(data.get(ATTR_FORECAST)) == 7
+async def test_temperature_convert(hass):
+ """Test temperature conversion."""
+ assert await async_setup_component(
+ hass, weather.DOMAIN, {"weather": {"platform": "demo"}}
+ )
+ hass.config.units = METRIC_SYSTEM
+ await hass.async_block_till_done()
- def test_temperature_convert(self):
- """Test temperature conversion."""
- state = self.hass.states.get("weather.demo_weather_north")
- assert state is not None
+ state = hass.states.get("weather.demo_weather_north")
+ assert state is not None
- assert state.state == "rainy"
+ assert state.state == "rainy"
- data = state.attributes
- assert data.get(ATTR_WEATHER_TEMPERATURE) == -24
+ data = state.attributes
+ assert data.get(ATTR_WEATHER_TEMPERATURE) == -24
diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py
index fcd024652e875c..30601ef6bec1dd 100644
--- a/tests/components/webhook/test_trigger.py
+++ b/tests/components/webhook/test_trigger.py
@@ -4,6 +4,8 @@
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
+from tests.async_mock import patch
+
@pytest.fixture(autouse=True)
async def setup_http(hass):
@@ -109,3 +111,60 @@ def store_event(event):
assert len(events) == 1
assert events[0].data["hello"] == "yo world"
+
+
+async def test_webhook_reload(hass, aiohttp_client):
+ """Test reloading a webhook."""
+ events = []
+
+ @callback
+ def store_event(event):
+ """Helepr to store events."""
+ events.append(event)
+
+ hass.bus.async_listen("test_success", store_event)
+
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": {
+ "trigger": {"platform": "webhook", "webhook_id": "post_webhook"},
+ "action": {
+ "event": "test_success",
+ "event_data_template": {"hello": "yo {{ trigger.data.hello }}"},
+ },
+ }
+ },
+ )
+
+ client = await aiohttp_client(hass.http.app)
+
+ await client.post("/api/webhook/post_webhook", data={"hello": "world"})
+
+ assert len(events) == 1
+ assert events[0].data["hello"] == "yo world"
+
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ autospec=True,
+ return_value={
+ "automation": {
+ "trigger": {"platform": "webhook", "webhook_id": "post_webhook"},
+ "action": {
+ "event": "test_success",
+ "event_data_template": {"hello": "yo2 {{ trigger.data.hello }}"},
+ },
+ }
+ },
+ ):
+ await hass.services.async_call(
+ "automation",
+ "reload",
+ blocking=True,
+ )
+
+ await client.post("/api/webhook/post_webhook", data={"hello": "world"})
+
+ assert len(events) == 2
+ assert events[1].data["hello"] == "yo2 world"
diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py
index 1b9eea86018538..e05108c46bcadf 100644
--- a/tests/components/websocket_api/test_commands.py
+++ b/tests/components/websocket_api/test_commands.py
@@ -201,10 +201,7 @@ async def test_get_states(hass, websocket_client):
states = []
for state in hass.states.async_all():
- state = state.as_dict()
- state["last_changed"] = state["last_changed"].isoformat()
- state["last_updated"] = state["last_updated"].isoformat()
- states.append(state)
+ states.append(state.as_dict())
assert msg["result"] == states
@@ -397,9 +394,7 @@ async def test_subscribe_unsubscribe_events_state_changed(
assert msg["event"]["data"]["entity_id"] == "light.permitted"
-async def test_render_template_renders_template(
- hass, websocket_client, hass_admin_user
-):
+async def test_render_template_renders_template(hass, websocket_client):
"""Test simple template is rendered and updated."""
hass.states.async_set("light.test", "on")
@@ -437,7 +432,7 @@ async def test_render_template_renders_template(
async def test_render_template_manual_entity_ids_no_longer_needed(
- hass, websocket_client, hass_admin_user
+ hass, websocket_client
):
"""Test that updates to specified entity ids cause a template rerender."""
hass.states.async_set("light.test", "on")
@@ -475,35 +470,96 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
}
-async def test_render_template_with_error(
- hass, websocket_client, hass_admin_user, caplog
-):
+async def test_render_template_with_error(hass, websocket_client, caplog):
"""Test a template with an error."""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": "{{ my_unknown_var() + 1 }}"}
)
msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert not msg["success"]
+ assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
+
+ assert "TemplateError" not in caplog.text
+
+
+async def test_render_template_with_delayed_error(hass, websocket_client, caplog):
+ """Test a template with an error that only happens after a state change."""
+ hass.states.async_set("sensor.test", "on")
+ await hass.async_block_till_done()
+
+ template_str = """
+{% if states.sensor.test.state %}
+ on
+{% else %}
+ {{ explode + 1 }}
+{% endif %}
+ """
+
+ await websocket_client.send_json(
+ {"id": 5, "type": "render_template", "template": template_str}
+ )
+ await hass.async_block_till_done()
+
+ msg = await websocket_client.receive_json()
+
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
+ hass.states.async_remove("sensor.test")
+ await hass.async_block_till_done()
+
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {
- "result": None,
- "listeners": {"all": True, "domains": [], "entities": []},
+ "result": "on",
+ "listeners": {"all": False, "domains": [], "entities": ["sensor.test"]},
}
- assert "my_unknown_var" in caplog.text
- assert "TemplateError" in caplog.text
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert not msg["success"]
+ assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
+ assert "TemplateError" not in caplog.text
-async def test_render_template_returns_with_match_all(
- hass, websocket_client, hass_admin_user
-):
+
+async def test_render_template_with_timeout(hass, websocket_client, caplog):
+ """Test a template that will timeout."""
+
+ slow_template_str = """
+{% for var in range(1000) -%}
+ {% for var in range(1000) -%}
+ {{ var }}
+ {%- endfor %}
+{%- endfor %}
+"""
+
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "render_template",
+ "timeout": 0.000001,
+ "template": slow_template_str,
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert not msg["success"]
+ assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
+
+ assert "TemplateError" not in caplog.text
+
+
+async def test_render_template_returns_with_match_all(hass, websocket_client):
"""Test that a template that would match with all entities still return success."""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": "State is: {{ 42 }}"}
diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py
new file mode 100644
index 00000000000000..832b72c5c1c0fb
--- /dev/null
+++ b/tests/components/websocket_api/test_messages.py
@@ -0,0 +1,65 @@
+"""Test Websocket API messages module."""
+
+from homeassistant.components.websocket_api.messages import (
+ cached_event_message,
+ message_to_json,
+)
+from homeassistant.const import EVENT_STATE_CHANGED
+from homeassistant.core import callback
+
+
+async def test_cached_event_message(hass):
+ """Test that we cache event messages."""
+
+ events = []
+
+ @callback
+ def _event_listener(event):
+ events.append(event)
+
+ hass.bus.async_listen(EVENT_STATE_CHANGED, _event_listener)
+
+ hass.states.async_set("light.window", "on")
+ hass.states.async_set("light.window", "off")
+ await hass.async_block_till_done()
+
+ assert len(events) == 2
+
+ msg0 = cached_event_message(2, events[0])
+ assert msg0 == cached_event_message(2, events[0])
+
+ msg1 = cached_event_message(2, events[1])
+ assert msg1 == cached_event_message(2, events[1])
+
+ assert msg0 != msg1
+
+ cache_info = cached_event_message.cache_info()
+ assert cache_info.hits == 2
+ assert cache_info.misses == 2
+ assert cache_info.currsize == 2
+
+ cached_event_message(2, events[1])
+ cache_info = cached_event_message.cache_info()
+ assert cache_info.hits == 3
+ assert cache_info.misses == 2
+ assert cache_info.currsize == 2
+
+
+async def test_message_to_json(caplog):
+ """Test we can serialize websocket messages."""
+
+ json_str = message_to_json({"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"}}'
+ )
+ assert "Unable to serialize to JSON" in caplog.text
+
+
+class _Unserializeable:
+ """A class that cannot be serialized."""
diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py
index 7ca6b3241ffbe1..c8476cbe3493e6 100644
--- a/tests/components/wilight/test_config_flow.py
+++ b/tests/components/wilight/test_config_flow.py
@@ -1,5 +1,4 @@
"""Test the WiLight config flow."""
-from asynctest import patch
import pytest
from homeassistant.components.wilight.config_flow import (
@@ -16,6 +15,7 @@
)
from homeassistant.helpers.typing import HomeAssistantType
+from tests.async_mock import patch
from tests.common import MockConfigEntry
from tests.components.wilight import (
CONF_COMPONENTS,
diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py
index 01ed57fdcd1daa..efd779a29df42d 100644
--- a/tests/components/wilight/test_init.py
+++ b/tests/components/wilight/test_init.py
@@ -1,5 +1,4 @@
"""Tests for the WiLight integration."""
-from asynctest import patch
import pytest
import pywilight
@@ -11,6 +10,7 @@
)
from homeassistant.helpers.typing import HomeAssistantType
+from tests.async_mock import patch
from tests.components.wilight import (
HOST,
UPNP_MAC_ADDRESS,
diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py
index 4d4a32604add43..d02c7233e60cb1 100644
--- a/tests/components/wilight/test_light.py
+++ b/tests/components/wilight/test_light.py
@@ -1,5 +1,4 @@
"""Tests for the WiLight integration."""
-from asynctest import patch
import pytest
import pywilight
@@ -17,6 +16,7 @@
)
from homeassistant.helpers.typing import HomeAssistantType
+from tests.async_mock import patch
from tests.components.wilight import (
HOST,
UPNP_MAC_ADDRESS,
diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py
index 487ccd9ab9e2eb..a39d1ef6453d65 100644
--- a/tests/components/wled/__init__.py
+++ b/tests/components/wled/__init__.py
@@ -3,7 +3,7 @@
import json
from homeassistant.components.wled.const import DOMAIN
-from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@@ -24,25 +24,25 @@ async def init_integration(
aioclient_mock.get(
"http://192.168.1.123:80/json/",
json=data,
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://192.168.1.123:80/json/state",
json=data["state"],
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://192.168.1.123:80/json/info",
json=data["info"],
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://192.168.1.123:80/json/state",
json=data["state"],
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py
index a0c5c11e7ce795..aed10ca3466d4c 100644
--- a/tests/components/wled/test_config_flow.py
+++ b/tests/components/wled/test_config_flow.py
@@ -5,7 +5,7 @@
from homeassistant import data_entry_flow
from homeassistant.components.wled import config_flow
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -45,7 +45,7 @@ async def test_show_zerconf_form(
aioclient_mock.get(
"http://192.168.1.123:80/json/",
text=load_fixture("wled/rgb.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.WLEDFlowHandler()
@@ -75,7 +75,7 @@ async def test_connection_error(
data={CONF_HOST: "example.com"},
)
- assert result["errors"] == {"base": "connection_error"}
+ assert result["errors"] == {"base": "cannot_connect"}
assert result["step_id"] == "user"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -93,7 +93,7 @@ async def test_zeroconf_connection_error(
data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
)
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -114,7 +114,7 @@ async def test_zeroconf_confirm_connection_error(
data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}},
)
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -127,7 +127,7 @@ async def test_zeroconf_no_data(
flow.hass = hass
result = await flow.async_step_zeroconf()
- assert result["reason"] == "connection_error"
+ assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -190,7 +190,7 @@ async def test_full_user_flow_implementation(
aioclient_mock.get(
"http://192.168.1.123:80/json/",
text=load_fixture("wled/rgb.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
@@ -218,7 +218,7 @@ async def test_full_zeroconf_flow_implementation(
aioclient_mock.get(
"http://192.168.1.123:80/json/",
text=load_fixture("wled/rgb.json"),
- headers={"Content-Type": "application/json"},
+ headers={"Content-Type": CONTENT_TYPE_JSON},
)
flow = config_flow.WLEDFlowHandler()
diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py
index 39cde51dbfd505..d5c1c738d2f395 100644
--- a/tests/components/wled/test_sensor.py
+++ b/tests/components/wled/test_sensor.py
@@ -12,7 +12,6 @@
ATTR_MAX_POWER,
CURRENT_MA,
DOMAIN,
- SIGNAL_DBM,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -20,6 +19,7 @@
ATTR_UNIT_OF_MEASUREMENT,
DATA_BYTES,
PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
@@ -138,7 +138,10 @@ async def test_sensors(
state = hass.states.get("sensor.wled_rgb_light_wifi_rssi")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:wifi"
- assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_DBM
+ assert (
+ state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ )
assert state.state == "-62"
entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi")
diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py
index e029ce783d32ad..8ed808b9d046af 100644
--- a/tests/components/worldclock/test_sensor.py
+++ b/tests/components/worldclock/test_sensor.py
@@ -1,49 +1,52 @@
"""The test for the World clock sensor platform."""
-import unittest
+import pytest
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from tests.common import get_test_home_assistant
+@pytest.fixture
+def time_zone():
+ """Fixture for time zone."""
+ return dt_util.get_time_zone("America/New_York")
-class TestWorldClockSensor(unittest.TestCase):
- """Test the World clock sensor."""
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.time_zone = dt_util.get_time_zone("America/New_York")
+async def test_time(hass, time_zone):
+ """Test the time at a different location."""
+ config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}}
- def test_time(self):
- """Test the time at a different location."""
- config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}}
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
- self.addCleanup(self.hass.stop)
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ config,
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.worldclock_sensor")
- assert state is not None
+ state = hass.states.get("sensor.worldclock_sensor")
+ assert state is not None
- assert state.state == dt_util.now(time_zone=self.time_zone).strftime("%H:%M")
+ assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M")
- def test_time_format(self):
- """Test time_format setting."""
- time_format = "%a, %b %d, %Y %I:%M %p"
- config = {
- "sensor": {
- "platform": "worldclock",
- "time_zone": "America/New_York",
- "time_format": time_format,
- }
+
+async def test_time_format(hass, time_zone):
+ """Test time_format setting."""
+ time_format = "%a, %b %d, %Y %I:%M %p"
+ config = {
+ "sensor": {
+ "platform": "worldclock",
+ "time_zone": "America/New_York",
+ "time_format": time_format,
}
- assert setup_component(self.hass, "sensor", config)
- self.hass.block_till_done()
- self.addCleanup(self.hass.stop)
+ }
+
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ config,
+ )
+ await hass.async_block_till_done()
- state = self.hass.states.get("sensor.worldclock_sensor")
- assert state is not None
+ state = hass.states.get("sensor.worldclock_sensor")
+ assert state is not None
- assert state.state == dt_util.now(time_zone=self.time_zone).strftime(
- time_format
- )
+ assert state.state == dt_util.now(time_zone=time_zone).strftime(time_format)
diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py
index b4fb30d25c5578..8709f5b6a4602f 100644
--- a/tests/components/wunderground/test_sensor.py
+++ b/tests/components/wunderground/test_sensor.py
@@ -3,7 +3,12 @@
from pytest import raises
import homeassistant.components.wunderground.sensor as wunderground
-from homeassistant.const import LENGTH_INCHES, STATE_UNKNOWN, TEMP_CELSIUS
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ LENGTH_INCHES,
+ STATE_UNKNOWN,
+ TEMP_CELSIUS,
+)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import async_setup_component
@@ -90,7 +95,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.pws_weather")
assert state.state == "Clear"
assert state.name == "Weather Summary"
- assert "unit_of_measurement" not in state.attributes
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert (
state.attributes["entity_picture"] == "https://icons.wxug.com/i/c/k/clear.gif"
)
@@ -114,7 +119,7 @@ async def test_sensor(hass, aioclient_mock):
assert state.state == "40"
assert state.name == "Feels Like"
assert "entity_picture" not in state.attributes
- assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
state = hass.states.get("sensor.pws_weather_1d_metric")
assert state.state == "Mostly Cloudy. Fog overnight."
@@ -123,7 +128,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.pws_precip_1d_in")
assert state.state == "0.03"
assert state.name == "Precipitation Intensity Today"
- assert state.attributes["unit_of_measurement"] == LENGTH_INCHES
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_INCHES
async def test_connect_failed(hass, aioclient_mock):
diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py
index d940e782be4402..742b983fa83982 100644
--- a/tests/components/yessssms/test_notify.py
+++ b/tests/components/yessssms/test_notify.py
@@ -1,9 +1,7 @@
"""The tests for the notify yessssms platform."""
import logging
-import unittest
import pytest
-import requests_mock
from homeassistant.components.yessssms.const import CONF_PROVIDER
import homeassistant.components.yessssms.notify as yessssms
@@ -151,210 +149,219 @@ async def test_connection_error_on_init(hass, caplog, valid_settings, connection
)
-class TestNotifyYesssSMS(unittest.TestCase):
- """Test the yessssms notify."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- login = "06641234567"
- passwd = "testpasswd"
- recipient = "06501234567"
- client = yessssms.YesssSMS(login, passwd)
- self.yessssms = yessssms.YesssSMSNotificationService(client, recipient)
-
- @requests_mock.Mocker()
- def test_login_error(self, mock):
- """Test login that fails."""
- mock.register_uri(
- requests_mock.POST,
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- status_code=200,
- text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
- )
-
- message = "Testing YesssSMS platform :)"
-
- with self.assertLogs("homeassistant.components.yessssms.notify", level="ERROR"):
- self.yessssms.send_message(message)
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 1)
-
- def test_empty_message_error(self):
- """Test for an empty SMS message error."""
- message = ""
- with self.assertLogs("homeassistant.components.yessssms.notify", level="ERROR"):
- self.yessssms.send_message(message)
-
- @requests_mock.Mocker()
- def test_error_account_suspended(self, mock):
- """Test login that fails after multiple attempts."""
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- status_code=200,
- text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
- )
-
- message = "Testing YesssSMS platform :)"
-
- with self.assertLogs("homeassistant.components.yessssms.notify", level="ERROR"):
- self.yessssms.send_message(message)
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 1)
-
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- status_code=200,
- text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für "
- "eine Stunde gesperrt.",
- )
-
- message = "Testing YesssSMS platform :)"
-
- with self.assertLogs("homeassistant.components.yessssms.notify", level="ERROR"):
- self.yessssms.send_message(message)
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 2)
-
- def test_error_account_suspended_2(self):
- """Test login that fails after multiple attempts."""
- message = "Testing YesssSMS platform :)"
+@pytest.fixture(name="yessssms")
+def yessssms_init():
+ """Set up things to be run when tests are started."""
+ login = "06641234567"
+ passwd = "testpasswd"
+ recipient = "06501234567"
+ client = yessssms.YesssSMS(login, passwd)
+ return yessssms.YesssSMSNotificationService(client, recipient)
+
+
+async def test_login_error(yessssms, requests_mock, caplog):
+ """Test login that fails."""
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._login_url,
+ status_code=200,
+ text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
+ )
+
+ message = "Testing YesssSMS platform :)"
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 1
+
+
+async def test_empty_message_error(yessssms, caplog):
+ """Test for an empty SMS message error."""
+ message = ""
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+
+ for record in caplog.records:
+ if (
+ record.levelname == "ERROR"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert "Cannot send empty SMS message" in record.message
+
+
+async def test_error_account_suspended(yessssms, requests_mock, caplog):
+ """Test login that fails after multiple attempts."""
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._login_url,
+ status_code=200,
+ text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
+ )
+
+ message = "Testing YesssSMS platform :)"
+
+ yessssms.send_message(message)
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 1
+
+ requests_mock.post(
# pylint: disable=protected-access
- self.yessssms.yesss._suspended = True
-
- with self.assertLogs(
- "homeassistant.components.yessssms.notify", level="ERROR"
- ) as context:
- self.yessssms.send_message(message)
- self.assertIn("Account is suspended, cannot send SMS.", context.output[0])
-
- @requests_mock.Mocker()
- def test_send_message(self, mock):
- """Test send message."""
- message = "Testing YesssSMS platform :)"
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- status_code=302,
- # pylint: disable=protected-access
- headers={"location": self.yessssms.yesss._kontomanager},
- )
+ yessssms.yesss._login_url,
+ status_code=200,
+ text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für "
+ "eine Stunde gesperrt.",
+ )
+
+ message = "Testing YesssSMS platform :)"
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 2
+
+
+async def test_error_account_suspended_2(yessssms, caplog):
+ """Test login that fails after multiple attempts."""
+ message = "Testing YesssSMS platform :)"
+ # pylint: disable=protected-access
+ yessssms.yesss._suspended = True
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+ for record in caplog.records:
+ if (
+ record.levelname == "ERROR"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert "Account is suspended, cannot send SMS." in record.message
+
+
+async def test_send_message(yessssms, requests_mock, caplog):
+ """Test send message."""
+ message = "Testing YesssSMS platform :)"
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._login_url,
+ status_code=302,
+ # pylint: disable=protected-access
+ headers={"location": yessssms.yesss._kontomanager},
+ )
+ # pylint: disable=protected-access
+ login = yessssms.yesss._logindata["login_rufnummer"]
+ requests_mock.get(
# pylint: disable=protected-access
- login = self.yessssms.yesss._logindata["login_rufnummer"]
- mock.register_uri(
- "GET",
- # pylint: disable=protected-access
- self.yessssms.yesss._kontomanager,
- status_code=200,
- text=f"test...{login}",
- )
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._websms_url,
- status_code=200,
- text="Ihre SMS wurde erfolgreich verschickt!
",
- )
- mock.register_uri(
- "GET",
- # pylint: disable=protected-access
- self.yessssms.yesss._logout_url,
- status_code=200,
- )
-
- with self.assertLogs(
- "homeassistant.components.yessssms.notify", level="INFO"
- ) as context:
- self.yessssms.send_message(message)
- self.assertIn("SMS sent", context.output[0])
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 4)
- self.assertIn(
- mock.last_request.scheme
- + "://"
- + mock.last_request.hostname
- + mock.last_request.path
- + "?"
- + mock.last_request.query,
- # pylint: disable=protected-access
- self.yessssms.yesss._logout_url,
- )
-
- def test_no_recipient_error(self):
- """Test for missing/empty recipient."""
- message = "Testing YesssSMS platform :)"
+ yessssms.yesss._kontomanager,
+ status_code=200,
+ text=f"test...{login}",
+ )
+ requests_mock.post(
# pylint: disable=protected-access
- self.yessssms._recipient = ""
-
- with self.assertLogs(
- "homeassistant.components.yessssms.notify", level="ERROR"
- ) as context:
- self.yessssms.send_message(message)
-
- self.assertIn(
- "You need to provide a recipient for SMS notification", context.output[0]
- )
-
- @requests_mock.Mocker()
- def test_sms_sending_error(self, mock):
- """Test sms sending error."""
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- status_code=302,
- # pylint: disable=protected-access
- headers={"location": self.yessssms.yesss._kontomanager},
- )
+ yessssms.yesss._websms_url,
+ status_code=200,
+ text="Ihre SMS wurde erfolgreich verschickt!
",
+ )
+ requests_mock.get(
# pylint: disable=protected-access
- login = self.yessssms.yesss._logindata["login_rufnummer"]
- mock.register_uri(
- "GET",
- # pylint: disable=protected-access
- self.yessssms.yesss._kontomanager,
- status_code=200,
- text=f"test...{login}",
- )
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._websms_url,
- status_code=HTTP_INTERNAL_SERVER_ERROR,
- )
-
- message = "Testing YesssSMS platform :)"
-
- with self.assertLogs(
- "homeassistant.components.yessssms.notify", level="ERROR"
- ) as context:
- self.yessssms.send_message(message)
-
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 3)
- self.assertIn("YesssSMS: error sending SMS", context.output[0])
-
- @requests_mock.Mocker()
- def test_connection_error(self, mock):
- """Test connection error."""
- mock.register_uri(
- "POST",
- # pylint: disable=protected-access
- self.yessssms.yesss._login_url,
- exc=yessssms.YesssSMS.ConnectionError,
- )
-
- message = "Testing YesssSMS platform :)"
-
- with self.assertLogs(
- "homeassistant.components.yessssms.notify", level="ERROR"
- ) as context:
- self.yessssms.send_message(message)
-
- self.assertTrue(mock.called)
- self.assertEqual(mock.call_count, 1)
- self.assertIn("cannot connect to provider", context.output[0])
+ yessssms.yesss._logout_url,
+ status_code=200,
+ )
+
+ with caplog.at_level(logging.INFO):
+ yessssms.send_message(message)
+ for record in caplog.records:
+ if (
+ record.levelname == "INFO"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert "SMS sent" in record.message
+
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 4
+ assert (
+ requests_mock.last_request.scheme
+ + "://"
+ + requests_mock.last_request.hostname
+ + requests_mock.last_request.path
+ + "?"
+ + requests_mock.last_request.query
+ ) in yessssms.yesss._logout_url # pylint: disable=protected-access
+
+
+async def test_no_recipient_error(yessssms, caplog):
+ """Test for missing/empty recipient."""
+ message = "Testing YesssSMS platform :)"
+ # pylint: disable=protected-access
+ yessssms._recipient = ""
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+ for record in caplog.records:
+ if (
+ record.levelname == "ERROR"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert (
+ "You need to provide a recipient for SMS notification" in record.message
+ )
+
+
+async def test_sms_sending_error(yessssms, requests_mock, caplog):
+ """Test sms sending error."""
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._login_url,
+ status_code=302,
+ # pylint: disable=protected-access
+ headers={"location": yessssms.yesss._kontomanager},
+ )
+ # pylint: disable=protected-access
+ login = yessssms.yesss._logindata["login_rufnummer"]
+ requests_mock.get(
+ # pylint: disable=protected-access
+ yessssms.yesss._kontomanager,
+ status_code=200,
+ text=f"test...{login}",
+ )
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._websms_url,
+ status_code=HTTP_INTERNAL_SERVER_ERROR,
+ )
+
+ message = "Testing YesssSMS platform :)"
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 3
+ for record in caplog.records:
+ if (
+ record.levelname == "ERROR"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert "YesssSMS: error sending SMS" in record.message
+
+
+async def test_connection_error(yessssms, requests_mock, caplog):
+ """Test connection error."""
+ requests_mock.post(
+ # pylint: disable=protected-access
+ yessssms.yesss._login_url,
+ exc=yessssms.yesss.ConnectionError,
+ )
+
+ message = "Testing YesssSMS platform :)"
+
+ with caplog.at_level(logging.ERROR):
+ yessssms.send_message(message)
+
+ assert requests_mock.called is True
+ assert requests_mock.call_count == 1
+ for record in caplog.records:
+ if (
+ record.levelname == "ERROR"
+ and record.name == "homeassistant.components.yessssms.notify"
+ ):
+ assert "cannot connect to provider" in record.message
diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py
deleted file mode 100644
index e7d7b030aafc1b..00000000000000
--- a/tests/components/zeroconf/conftest.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""conftest for zeroconf."""
-import pytest
-
-from tests.async_mock import patch
-
-
-@pytest.fixture
-def mock_zeroconf():
- """Mock zeroconf."""
- with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
- yield mock_zc.return_value
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index ae1f6d5fd98a98..8767953b36385f 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -9,7 +9,11 @@
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
-from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_START,
+ EVENT_HOMEASSISTANT_STARTED,
+ EVENT_HOMEASSISTANT_STOP,
+)
from homeassistant.generated import zeroconf as zc_gen
from homeassistant.setup import async_setup_component
@@ -128,7 +132,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
"""Test we still setup with long urls and names."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
- ) as mock_service_browser, patch(
+ ), patch(
"homeassistant.components.zeroconf.get_url",
return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value",
), patch.object(
@@ -138,10 +142,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
):
mock_zeroconf.get_service_info.side_effect = get_service_info_mock
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
- hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
- assert len(mock_service_browser.mock_calls) == 1
assert "https://this.url.is.way.too.long" in caplog.text
assert "German Umlaut" in caplog.text
@@ -461,6 +464,7 @@ async def test_info_from_service_with_addresses(hass):
async def test_get_instance(hass, mock_zeroconf):
"""Test we get an instance."""
+ assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py
index e45a93a38b3b6c..9cf953f4c8dff7 100644
--- a/tests/components/zeroconf/test_usage.py
+++ b/tests/components/zeroconf/test_usage.py
@@ -3,12 +3,16 @@
from homeassistant.components.zeroconf import async_get_instance
from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher
+from homeassistant.setup import async_setup_component
from tests.async_mock import Mock, patch
+DOMAIN = "zeroconf"
+
async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
"""Test creating multiple zeroconf throws without an integration."""
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
zeroconf_instance = await async_get_instance(hass)
@@ -22,6 +26,7 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog):
async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog):
"""Test creating multiple zeroconf gives the shared instance to an integration."""
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
zeroconf_instance = await async_get_instance(hass)
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index 82799e8dd9ddf4..11f390b202ed3e 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -108,6 +108,7 @@ def __init__(self, app, ieee, manufacturer, model, node_desc=None, nwk=0xB79C):
if node_desc is None:
node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00"
self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0]
+ self.neighbors = []
FakeDevice.add_to_group = zigpy_dev.add_to_group
diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py
index 0587bd14c8c5fb..53861bc0d9a4e7 100644
--- a/tests/components/zha/test_api.py
+++ b/tests/components/zha/test_api.py
@@ -1,28 +1,48 @@
"""Test ZHA API."""
+from binascii import unhexlify
import pytest
+import voluptuous as vol
import zigpy.profiles.zha
+import zigpy.types
import zigpy.zcl.clusters.general as general
from homeassistant.components.websocket_api import const
-from homeassistant.components.zha.api import ID, TYPE, async_load_api
+from homeassistant.components.zha import DOMAIN
+from homeassistant.components.zha.api import (
+ ATTR_DURATION,
+ ATTR_INSTALL_CODE,
+ ATTR_QR_CODE,
+ ATTR_SOURCE_IEEE,
+ ID,
+ SERVICE_PERMIT,
+ TYPE,
+ async_load_api,
+)
from homeassistant.components.zha.core.const import (
ATTR_CLUSTER_ID,
ATTR_CLUSTER_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_ENDPOINT_NAMES,
ATTR_IEEE,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
+ ATTR_NEIGHBORS,
ATTR_QUIRK_APPLIED,
CLUSTER_TYPE_IN,
+ DATA_ZHA,
+ DATA_ZHA_GATEWAY,
GROUP_ID,
GROUP_IDS,
GROUP_NAME,
)
+from homeassistant.core import Context
from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME
+from tests.async_mock import AsyncMock, patch
+
IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7"
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
@@ -166,6 +186,8 @@ async def test_list_devices(zha_client):
assert device[ATTR_NAME] is not None
assert device[ATTR_QUIRK_APPLIED] is not None
assert device["entities"] is not None
+ assert device[ATTR_NEIGHBORS] is not None
+ assert device[ATTR_ENDPOINT_NAMES] is not None
for entity_reference in device["entities"]:
assert entity_reference[ATTR_NAME] is not None
@@ -225,7 +247,7 @@ async def test_get_group(zha_client):
async def test_get_group_not_found(zha_client):
"""Test not found response from get group API."""
- await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567})
+ await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1_234_567})
msg = await zha_client.receive_json()
@@ -335,3 +357,244 @@ async def test_remove_group(zha_client):
groups = msg["result"]
assert len(groups) == 0
+
+
+@pytest.fixture
+async def app_controller(hass, setup_zha):
+ """Fixture for zigpy Application Controller."""
+ await setup_zha()
+ controller = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].application_controller
+ p1 = patch.object(controller, "permit")
+ p2 = patch.object(controller, "permit_with_key", new=AsyncMock())
+ with p1, p2:
+ yield controller
+
+
+@pytest.mark.parametrize(
+ "params, duration, node",
+ (
+ ({}, 60, None),
+ ({ATTR_DURATION: 30}, 30, None),
+ (
+ {ATTR_DURATION: 33, ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:dd"},
+ 33,
+ zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:dd"),
+ ),
+ (
+ {ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:d1"},
+ 60,
+ zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"),
+ ),
+ ),
+)
+async def test_permit_ha12(
+ hass, app_controller, hass_admin_user, params, duration, node
+):
+ """Test permit service."""
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
+ )
+ assert app_controller.permit.await_count == 1
+ assert app_controller.permit.await_args[1]["time_s"] == duration
+ assert app_controller.permit.await_args[1]["node"] == node
+ assert app_controller.permit_with_key.call_count == 0
+
+
+IC_TEST_PARAMS = (
+ (
+ {
+ ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
+ ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051",
+ },
+ zigpy.types.EUI64.convert(IEEE_SWITCH_DEVICE),
+ unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
+ ),
+ (
+ {
+ ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
+ ATTR_INSTALL_CODE: "52797BF4A5084DAA8E1712B61741CA024051",
+ },
+ zigpy.types.EUI64.convert(IEEE_SWITCH_DEVICE),
+ unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
+ ),
+)
+
+
+@pytest.mark.parametrize("params, src_ieee, code", IC_TEST_PARAMS)
+async def test_permit_with_install_code(
+ hass, app_controller, hass_admin_user, params, src_ieee, code
+):
+ """Test permit service with install code."""
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
+ )
+ assert app_controller.permit.await_count == 0
+ assert app_controller.permit_with_key.call_count == 1
+ assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
+ assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
+ assert app_controller.permit_with_key.await_args[1]["code"] == code
+
+
+IC_FAIL_PARAMS = (
+ {
+ # wrong install code
+ ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
+ ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4052",
+ },
+ # incorrect service params
+ {ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051"},
+ {ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE},
+ {
+ # incorrect service params
+ ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051",
+ ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051",
+ },
+ {
+ # incorrect service params
+ ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
+ ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051",
+ },
+ {
+ # good regex match, but bad code
+ ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024052"
+ },
+ {
+ # good aqara regex match, but bad code
+ ATTR_QR_CODE: (
+ "G$M:751$S:357S00001579$D:000000000F350FFD%Z$A:04CF8CDF"
+ "3C3C3C3C$I:52797BF4A5084DAA8E1712B61741CA024052"
+ )
+ },
+ # good consciot regex match, but bad code
+ {ATTR_QR_CODE: "000D6FFFFED4163B|52797BF4A5084DAA8E1712B61741CA024052"},
+)
+
+
+@pytest.mark.parametrize("params", IC_FAIL_PARAMS)
+async def test_permit_with_install_code_fail(
+ hass, app_controller, hass_admin_user, params
+):
+ """Test permit service with install code."""
+
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
+ )
+ assert app_controller.permit.await_count == 0
+ assert app_controller.permit_with_key.call_count == 0
+
+
+IC_QR_CODE_TEST_PARAMS = (
+ (
+ {ATTR_QR_CODE: "000D6FFFFED4163B|52797BF4A5084DAA8E1712B61741CA024051"},
+ zigpy.types.EUI64.convert("00:0D:6F:FF:FE:D4:16:3B"),
+ unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
+ ),
+ (
+ {ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051"},
+ zigpy.types.EUI64.convert("00:0D:6F:FF:FE:D4:16:3B"),
+ unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
+ ),
+ (
+ {
+ ATTR_QR_CODE: (
+ "G$M:751$S:357S00001579$D:000000000F350FFD%Z$A:04CF8CDF"
+ "3C3C3C3C$I:52797BF4A5084DAA8E1712B61741CA024051"
+ )
+ },
+ zigpy.types.EUI64.convert("04:CF:8C:DF:3C:3C:3C:3C"),
+ unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
+ ),
+)
+
+
+@pytest.mark.parametrize("params, src_ieee, code", IC_QR_CODE_TEST_PARAMS)
+async def test_permit_with_qr_code(
+ hass, app_controller, hass_admin_user, params, src_ieee, code
+):
+ """Test permit service with install code from qr code."""
+
+ await hass.services.async_call(
+ DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
+ )
+ assert app_controller.permit.await_count == 0
+ assert app_controller.permit_with_key.call_count == 1
+ assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
+ assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
+ assert app_controller.permit_with_key.await_args[1]["code"] == code
+
+
+@pytest.mark.parametrize("params, src_ieee, code", IC_QR_CODE_TEST_PARAMS)
+async def test_ws_permit_with_qr_code(
+ app_controller, zha_client, params, src_ieee, code
+):
+ """Test permit service with install code from qr code."""
+
+ await zha_client.send_json(
+ {ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
+ )
+
+ msg = await zha_client.receive_json()
+ assert msg["id"] == 14
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+
+ assert app_controller.permit.await_count == 0
+ assert app_controller.permit_with_key.call_count == 1
+ assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
+ assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
+ assert app_controller.permit_with_key.await_args[1]["code"] == code
+
+
+@pytest.mark.parametrize("params", IC_FAIL_PARAMS)
+async def test_ws_permit_with_install_code_fail(app_controller, zha_client, params):
+ """Test permit ws service with install code."""
+
+ await zha_client.send_json(
+ {ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
+ )
+
+ msg = await zha_client.receive_json()
+ assert msg["id"] == 14
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"] is False
+
+ assert app_controller.permit.await_count == 0
+ assert app_controller.permit_with_key.call_count == 0
+
+
+@pytest.mark.parametrize(
+ "params, duration, node",
+ (
+ ({}, 60, None),
+ ({ATTR_DURATION: 30}, 30, None),
+ (
+ {ATTR_DURATION: 33, ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:dd"},
+ 33,
+ zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:dd"),
+ ),
+ (
+ {ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:d1"},
+ 60,
+ zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"),
+ ),
+ ),
+)
+async def test_ws_permit_ha12(app_controller, zha_client, params, duration, node):
+ """Test permit ws service."""
+
+ await zha_client.send_json(
+ {ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
+ )
+
+ msg = await zha_client.receive_json()
+ assert msg["id"] == 14
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+
+ assert app_controller.permit.await_count == 1
+ assert app_controller.permit.await_args[1]["time_s"] == duration
+ assert app_controller.permit.await_args[1]["node"] == node
+ assert app_controller.permit_with_key.call_count == 0
diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py
index afa86e90f2cd81..7a2217521ad6db 100644
--- a/tests/components/zha/test_binary_sensor.py
+++ b/tests/components/zha/test_binary_sensor.py
@@ -1,5 +1,6 @@
"""Test zha binary sensor."""
import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.security as security
@@ -15,7 +16,7 @@
DEVICE_IAS = {
1: {
- "device_type": 1026,
+ "device_type": zigpy.profiles.zha.DeviceType.IAS_ZONE,
"in_clusters": [security.IasZone.cluster_id],
"out_clusters": [],
}
@@ -24,7 +25,7 @@
DEVICE_OCCUPANCY = {
1: {
- "device_type": 263,
+ "device_type": zigpy.profiles.zha.DeviceType.OCCUPANCY_SENSOR,
"in_clusters": [measurement.OccupancySensing.cluster_id],
"out_clusters": [],
}
diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py
index 471e44f8409b1b..8a6d934d3739ff 100644
--- a/tests/components/zha/test_channels.py
+++ b/tests/components/zha/test_channels.py
@@ -3,6 +3,7 @@
from unittest import mock
import pytest
+import zigpy.profiles.zha
import zigpy.types as t
import zigpy.zcl.clusters
@@ -286,7 +287,11 @@ def test_ep_channels_all_channels(m1, zha_device_mock):
"""Test EndpointChannels adding all channels."""
zha_device = zha_device_mock(
{
- 1: {"in_clusters": [0, 1, 6, 8], "out_clusters": [], "device_type": 0x0000},
+ 1: {
+ "in_clusters": [0, 1, 6, 8],
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
+ },
2: {
"in_clusters": [0, 1, 6, 8, 768],
"out_clusters": [],
diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py
index c6c0e74050dd07..b295543b3e8f1b 100644
--- a/tests/components/zha/test_cover.py
+++ b/tests/components/zha/test_cover.py
@@ -2,6 +2,7 @@
import asyncio
import pytest
+import zigpy.profiles.zha
import zigpy.types
import zigpy.zcl.clusters.closures as closures
import zigpy.zcl.clusters.general as general
@@ -41,7 +42,7 @@ def zigpy_cover_device(zigpy_device_mock):
endpoints = {
1: {
- "device_type": 1026,
+ "device_type": zigpy.profiles.zha.DeviceType.IAS_ZONE,
"in_clusters": [closures.WindowCovering.cluster_id],
"out_clusters": [],
}
@@ -55,7 +56,7 @@ def zigpy_cover_remote(zigpy_device_mock):
endpoints = {
1: {
- "device_type": 0x0203,
+ "device_type": zigpy.profiles.zha.DeviceType.WINDOW_COVERING_CONTROLLER,
"in_clusters": [],
"out_clusters": [closures.WindowCovering.cluster_id],
}
@@ -69,7 +70,7 @@ def zigpy_shade_device(zigpy_device_mock):
endpoints = {
1: {
- "device_type": 512,
+ "device_type": zigpy.profiles.zha.DeviceType.SHADE,
"in_clusters": [
closures.Shade.cluster_id,
general.LevelControl.cluster_id,
@@ -87,7 +88,7 @@ def zigpy_keen_vent(zigpy_device_mock):
endpoints = {
1: {
- "device_type": 3,
+ "device_type": zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT,
"in_clusters": [general.LevelControl.cluster_id, general.OnOff.cluster_id],
"out_clusters": [],
}
diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py
index 83a57f1fabf4a1..1cc9fb27d897bb 100644
--- a/tests/components/zha/test_device.py
+++ b/tests/components/zha/test_device.py
@@ -4,6 +4,7 @@
from unittest import mock
import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import homeassistant.components.zha.core.device as zha_core_device
@@ -27,7 +28,11 @@ def _dev(with_basic_channel: bool = True):
in_clusters.append(general.Basic.cluster_id)
endpoints = {
- 3: {"in_clusters": in_clusters, "out_clusters": [], "device_type": 0}
+ 3: {
+ "in_clusters": in_clusters,
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
+ }
}
return zigpy_device_mock(endpoints)
@@ -44,7 +49,11 @@ def _dev(with_basic_channel: bool = True):
in_clusters.append(general.Basic.cluster_id)
endpoints = {
- 3: {"in_clusters": in_clusters, "out_clusters": [], "device_type": 0}
+ 3: {
+ "in_clusters": in_clusters,
+ "out_clusters": [],
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
+ }
}
return zigpy_device_mock(
endpoints, node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00"
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index 40e64934f89c66..de3a4eb1296dca 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -2,6 +2,7 @@
from unittest.mock import patch
import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.security as security
import zigpy.zcl.foundation as zcl_f
@@ -31,7 +32,7 @@ async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored):
1: {
"in_clusters": [c.cluster_id for c in clusters],
"out_clusters": [general.OnOff.cluster_id],
- "device_type": 0,
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
},
)
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index e0a73903e0dd6f..801b6831379ffa 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -3,6 +3,7 @@
import time
import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import homeassistant.components.automation as automation
@@ -58,7 +59,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored):
1: {
"in_clusters": [general.Basic.cluster_id],
"out_clusters": [general.OnOff.cluster_id],
- "device_type": 0,
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
)
diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py
index 0cd8c58e49fe05..5589a7d94ac4a3 100644
--- a/tests/components/zha/test_discover.py
+++ b/tests/components/zha/test_discover.py
@@ -4,6 +4,7 @@
from unittest import mock
import pytest
+import zigpy.profiles.zha
import zigpy.quirks
import zigpy.types
import zigpy.zcl.clusters.closures
@@ -163,9 +164,9 @@ def test_discover_entities(m1, m2):
@pytest.mark.parametrize(
"device_type, component, hit",
[
- (0x0100, zha_const.LIGHT, True),
- (0x0108, zha_const.SWITCH, True),
- (0x0051, zha_const.SWITCH, True),
+ (zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_const.LIGHT, True),
+ (zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, zha_const.SWITCH, True),
+ (zigpy.profiles.zha.DeviceType.SMART_PLUG, zha_const.SWITCH, True),
(0xFFFF, None, False),
],
)
@@ -379,7 +380,7 @@ async def test_device_override(
zigpy_device = zigpy_device_mock(
{
1: {
- "device_type": 258,
+ "device_type": zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT,
"endpoint_id": 1,
"in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513],
"out_clusters": [25],
diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py
index b163edbd49abac..b12b9249373829 100644
--- a/tests/components/zha/test_fan.py
+++ b/tests/components/zha/test_fan.py
@@ -43,7 +43,11 @@
def zigpy_device(zigpy_device_mock):
"""Device tracker zigpy device."""
endpoints = {
- 1: {"in_clusters": [hvac.Fan.cluster_id], "out_clusters": [], "device_type": 0}
+ 1: {
+ "in_clusters": [hvac.Fan.cluster_id],
+ "out_clusters": [],
+ "device_type": zha.DeviceType.ON_OFF_SWITCH,
+ }
}
return zigpy_device_mock(endpoints)
diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py
index cc9e811cb495b6..718c7cdba19b00 100644
--- a/tests/components/zha/test_gateway.py
+++ b/tests/components/zha/test_gateway.py
@@ -28,7 +28,7 @@ def zigpy_dev_basic(zigpy_device_mock):
1: {
"in_clusters": [general.Basic.cluster_id],
"out_clusters": [],
- "device_type": 0,
+ "device_type": zha.DeviceType.ON_OFF_SWITCH,
}
}
)
diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py
index bd340445527a91..642504384cc314 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -34,7 +34,7 @@
LIGHT_ON_OFF = {
1: {
- "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT,
+ "device_type": zha.DeviceType.ON_OFF_LIGHT,
"in_clusters": [
general.Basic.cluster_id,
general.Identify.cluster_id,
@@ -46,7 +46,7 @@
LIGHT_LEVEL = {
1: {
- "device_type": zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT,
+ "device_type": zha.DeviceType.DIMMABLE_LIGHT,
"in_clusters": [
general.Basic.cluster_id,
general.LevelControl.cluster_id,
@@ -58,7 +58,7 @@
LIGHT_COLOR = {
1: {
- "device_type": zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+ "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT,
"in_clusters": [
general.Basic.cluster_id,
general.Identify.cluster_id,
diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py
index 7cba51b3e5c2d5..8d854894ba0f3d 100644
--- a/tests/components/zha/test_sensor.py
+++ b/tests/components/zha/test_sensor.py
@@ -2,6 +2,7 @@
from unittest import mock
import pytest
+import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.homeautomation as homeautomation
import zigpy.zcl.clusters.measurement as measurement
@@ -14,8 +15,10 @@
CONF_UNIT_SYSTEM,
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
TEMP_CELSIUS,
@@ -48,16 +51,16 @@ async def async_test_temperature(hass, cluster, entity_id):
async def async_test_pressure(hass, cluster, entity_id):
"""Test pressure sensor."""
await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000})
- assert_state(hass, entity_id, "1000", "hPa")
+ assert_state(hass, entity_id, "1000", PRESSURE_HPA)
await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000})
- assert_state(hass, entity_id, "1000", "hPa")
+ assert_state(hass, entity_id, "1000", PRESSURE_HPA)
async def async_test_illuminance(hass, cluster, entity_id):
"""Test illuminance sensor."""
await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20})
- assert_state(hass, entity_id, "1.0", "lx")
+ assert_state(hass, entity_id, "1.0", LIGHT_LUX)
async def async_test_metering(hass, cluster, entity_id):
@@ -120,7 +123,7 @@ async def test_sensor(
1: {
"in_clusters": [cluster_id, general.Basic.cluster_id],
"out_cluster": [],
- "device_type": 0x0000,
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
)
@@ -240,7 +243,7 @@ async def test_temp_uom(
general.Basic.cluster_id,
],
"out_cluster": [],
- "device_type": 0x0000,
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
)
@@ -280,7 +283,7 @@ async def test_electrical_measurement_init(
1: {
"in_clusters": [cluster_id, general.Basic.cluster_id],
"out_cluster": [],
- "device_type": 0x0000,
+ "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
)
diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py
index b1c0c643bbc84d..aab8dafef4f409 100644
--- a/tests/components/zha/test_switch.py
+++ b/tests/components/zha/test_switch.py
@@ -33,7 +33,7 @@ def zigpy_device(zigpy_device_mock):
1: {
"in_clusters": [general.Basic.cluster_id, general.OnOff.cluster_id],
"out_clusters": [],
- "device_type": 0,
+ "device_type": zha.DeviceType.ON_OFF_SWITCH,
}
}
return zigpy_device_mock(endpoints)
diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py
index 6de4bd79a72909..a6287592395d51 100644
--- a/tests/components/zha/zha_devices_list.py
+++ b/tests/components/zha/zha_devices_list.py
@@ -784,7 +784,7 @@
},
("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): {
"channels": ["on_off"],
- "entity_class": "Opening",
+ "entity_class": "Motion",
"entity_id": "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off",
},
},
@@ -3464,4 +3464,248 @@
"model": "unk_model",
"node_descriptor": b"\x01@\x8e\x10\x11RR\x00\x00\x00R\x00\x00",
},
+ {
+ "device_no": 98,
+ "endpoints": {
+ 208: {
+ "endpoint_id": 208,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000C],
+ "out_clusters": [],
+ },
+ 209: {
+ "endpoint_id": 209,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000C],
+ "out_clusters": [],
+ },
+ 210: {
+ "endpoint_id": 210,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000C],
+ "out_clusters": [],
+ },
+ 211: {
+ "endpoint_id": 211,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000C],
+ "out_clusters": [],
+ },
+ 212: {
+ "endpoint_id": 212,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 213: {
+ "endpoint_id": 213,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 214: {
+ "endpoint_id": 214,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 215: {
+ "endpoint_id": 215,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000C],
+ "out_clusters": [],
+ },
+ 216: {
+ "endpoint_id": 216,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 217: {
+ "endpoint_id": 217,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 218: {
+ "endpoint_id": 218,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000D],
+ "out_clusters": [],
+ },
+ 219: {
+ "endpoint_id": 219,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006, 0x000D],
+ "out_clusters": [],
+ },
+ 220: {
+ "endpoint_id": 220,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 221: {
+ "endpoint_id": 221,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 222: {
+ "endpoint_id": 222,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0006],
+ "out_clusters": [],
+ },
+ 232: {
+ "endpoint_id": 232,
+ "profile_id": 49413,
+ "device_type": 0x0001,
+ "in_clusters": [0x0011, 0x0092],
+ "out_clusters": [0x0008, 0x0011],
+ },
+ },
+ "entities": [
+ "switch.digi_xbee3_77665544_on_off",
+ "switch.digi_xbee3_77665544_on_off_2",
+ "switch.digi_xbee3_77665544_on_off_3",
+ "switch.digi_xbee3_77665544_on_off_4",
+ "switch.digi_xbee3_77665544_on_off_5",
+ "switch.digi_xbee3_77665544_on_off_6",
+ "switch.digi_xbee3_77665544_on_off_7",
+ "switch.digi_xbee3_77665544_on_off_8",
+ "switch.digi_xbee3_77665544_on_off_9",
+ "switch.digi_xbee3_77665544_on_off_10",
+ "switch.digi_xbee3_77665544_on_off_11",
+ "switch.digi_xbee3_77665544_on_off_12",
+ "switch.digi_xbee3_77665544_on_off_13",
+ "switch.digi_xbee3_77665544_on_off_14",
+ "switch.digi_xbee3_77665544_on_off_15",
+ "sensor.digi_xbee3_77665544_analog_input",
+ "sensor.digi_xbee3_77665544_analog_input_2",
+ "sensor.digi_xbee3_77665544_analog_input_3",
+ "sensor.digi_xbee3_77665544_analog_input_4",
+ ],
+ "entity_map": {
+ ("switch", "00:11:22:33:44:55:66:77-208-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-209-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_2",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-210-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_3",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-211-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_4",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-212-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_5",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-213-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_6",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-214-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_7",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-215-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_8",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-216-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_9",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-217-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_10",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-218-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_11",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-219-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_12",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-220-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_13",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-221-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_14",
+ },
+ ("switch", "00:11:22:33:44:55:66:77-222-6"): {
+ "channels": ["on_off"],
+ "entity_class": "Switch",
+ "entity_id": "switch.digi_xbee3_77665544_on_off_15",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-208-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.digi_xbee3_77665544_analog_input",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-209-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.digi_xbee3_77665544_analog_input_2",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-210-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.digi_xbee3_77665544_analog_input_3",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-211-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.digi_xbee3_77665544_analog_input_4",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-215-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.digi_xbee3_77665544_analog_input_5",
+ },
+ },
+ "event_channels": ["232:0x0008"],
+ "manufacturer": "Digi",
+ "model": "XBee3",
+ "node_descriptor": b"\x01@\x8e\x1e\x10R\xff\x00\x00,\xff\x00\x00",
+ },
]
diff --git a/tests/components/zodiac/__init__.py b/tests/components/zodiac/__init__.py
new file mode 100644
index 00000000000000..e9bae20c4424f5
--- /dev/null
+++ b/tests/components/zodiac/__init__.py
@@ -0,0 +1 @@
+"""Tests for the zodiac component."""
diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py
new file mode 100644
index 00000000000000..b08352dd2c0b0e
--- /dev/null
+++ b/tests/components/zodiac/test_sensor.py
@@ -0,0 +1,50 @@
+"""The test for the zodiac sensor platform."""
+from datetime import datetime
+
+import pytest
+
+from homeassistant.components.zodiac.const import (
+ ATTR_ELEMENT,
+ ATTR_MODALITY,
+ DOMAIN,
+ ELEMENT_EARTH,
+ ELEMENT_FIRE,
+ ELEMENT_WATER,
+ MODALITY_CARDINAL,
+ MODALITY_FIXED,
+ SIGN_ARIES,
+ SIGN_SCORPIO,
+ SIGN_TAURUS,
+)
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.async_mock import patch
+
+DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC)
+DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC)
+DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC)
+
+
+@pytest.mark.parametrize(
+ "now,sign,element,modality",
+ [
+ (DAY1, SIGN_SCORPIO, ELEMENT_WATER, MODALITY_FIXED),
+ (DAY2, SIGN_ARIES, ELEMENT_FIRE, MODALITY_CARDINAL),
+ (DAY3, SIGN_TAURUS, ELEMENT_EARTH, MODALITY_FIXED),
+ ],
+)
+async def test_zodiac_day(hass, now, sign, element, modality):
+ """Test the zodiac sensor."""
+ config = {DOMAIN: {}}
+
+ with patch("homeassistant.components.zodiac.sensor.utcnow", return_value=now):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.zodiac")
+ assert state
+ assert state.state == sign
+ assert state.attributes
+ assert state.attributes[ATTR_ELEMENT] == element
+ assert state.attributes[ATTR_MODALITY] == modality
diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py
index e80f70b10fea49..0477f9bead7f86 100644
--- a/tests/components/zone/test_trigger.py
+++ b/tests/components/zone/test_trigger.py
@@ -2,11 +2,11 @@
import pytest
from homeassistant.components import automation, zone
+from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
-from tests.components.automation import common
@pytest.fixture
@@ -91,8 +91,12 @@ async def test_if_fires_on_zone_enter(hass, calls):
)
await hass.async_block_till_done()
- await common.async_turn_off(hass)
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
diff --git a/tests/components/zoneminder/__init__.py b/tests/components/zoneminder/__init__.py
new file mode 100644
index 00000000000000..9ea5189a7b9fa1
--- /dev/null
+++ b/tests/components/zoneminder/__init__.py
@@ -0,0 +1 @@
+"""Tests for the zoneminder component."""
diff --git a/tests/components/zoneminder/test_binary_sensor.py b/tests/components/zoneminder/test_binary_sensor.py
new file mode 100644
index 00000000000000..ee9883283e96c9
--- /dev/null
+++ b/tests/components/zoneminder/test_binary_sensor.py
@@ -0,0 +1,65 @@
+"""Binary sensor tests."""
+from zoneminder.zm import ZoneMinder
+
+from homeassistant.components.zoneminder import const
+from homeassistant.config import async_process_ha_core_config
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import MagicMock, patch
+from tests.common import MockConfigEntry
+
+
+async def test_async_setup_entry(hass: HomeAssistant) -> None:
+ """Test setup of binary sensor entities."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.is_available = True
+
+ zoneminder_mock.return_value = zm_client
+
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id="host1",
+ data={
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ await async_process_ha_core_config(hass, {})
+ await async_setup_component(hass, HASS_DOMAIN, {})
+ await async_setup_component(hass, const.DOMAIN, {})
+ await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("binary_sensor.host1").state == "on"
+
+ zm_client.is_available = False
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("binary_sensor.host1").state == "off"
diff --git a/tests/components/zoneminder/test_camera.py b/tests/components/zoneminder/test_camera.py
new file mode 100644
index 00000000000000..06f4c3554dfdd0
--- /dev/null
+++ b/tests/components/zoneminder/test_camera.py
@@ -0,0 +1,89 @@
+"""Binary sensor tests."""
+from zoneminder.monitor import Monitor
+from zoneminder.zm import ZoneMinder
+
+from homeassistant.components.zoneminder import const
+from homeassistant.config import async_process_ha_core_config
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import MagicMock, patch
+from tests.common import MockConfigEntry
+
+
+async def test_async_setup_entry(hass: HomeAssistant) -> None:
+ """Test setup of camera entities."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ monitor1 = MagicMock(spec=Monitor)
+ monitor1.name = "monitor1"
+ monitor1.mjpeg_image_url = "mjpeg_image_url1"
+ monitor1.still_image_url = "still_image_url1"
+ monitor1.is_recording = True
+ monitor1.is_available = True
+
+ monitor2 = MagicMock(spec=Monitor)
+ monitor2.name = "monitor2"
+ monitor2.mjpeg_image_url = "mjpeg_image_url2"
+ monitor2.still_image_url = "still_image_url2"
+ monitor2.is_recording = False
+ monitor2.is_available = False
+
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.get_monitors.return_value = [monitor1, monitor2]
+
+ zoneminder_mock.return_value = zm_client
+
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id="host1",
+ data={
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ await async_process_ha_core_config(hass, {})
+ await async_setup_component(hass, HASS_DOMAIN, {})
+ await async_setup_component(hass, const.DOMAIN, {})
+ await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("camera.monitor1").state == "recording"
+ assert hass.states.get("camera.monitor2").state == "unavailable"
+
+ monitor1.is_recording = False
+ monitor2.is_recording = True
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("camera.monitor1").state == "idle"
+ assert hass.states.get("camera.monitor2").state == "unavailable"
diff --git a/tests/components/zoneminder/test_config_flow.py b/tests/components/zoneminder/test_config_flow.py
new file mode 100644
index 00000000000000..d62183cd0f6361
--- /dev/null
+++ b/tests/components/zoneminder/test_config_flow.py
@@ -0,0 +1,119 @@
+"""Config flow tests."""
+import requests
+from zoneminder.zm import ZoneMinder
+
+from homeassistant import config_entries
+from homeassistant.components.zoneminder import ClientAvailabilityResult, const
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SOURCE,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import HomeAssistant
+
+from tests.async_mock import MagicMock, patch
+
+
+async def test_import(hass: HomeAssistant) -> None:
+ """Test import from configuration yaml."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ conf_data = {
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ }
+
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zoneminder_mock.return_value = zm_client
+
+ zm_client.login.return_value = False
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=conf_data,
+ )
+ assert result
+ assert result["type"] == "abort"
+ assert result["reason"] == "invalid_auth"
+
+ zm_client.login.return_value = True
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=conf_data,
+ )
+ assert result
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ **conf_data,
+ CONF_SOURCE: config_entries.SOURCE_IMPORT,
+ }
+
+
+async def test_user(hass: HomeAssistant) -> None:
+ """Test user initiated creation."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ conf_data = {
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result
+ assert result["type"] == "form"
+
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zoneminder_mock.return_value = zm_client
+
+ zm_client.login.side_effect = requests.exceptions.ConnectionError()
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ conf_data,
+ )
+ assert result
+ assert result["type"] == "form"
+ assert result["errors"] == {
+ "base": ClientAvailabilityResult.ERROR_CONNECTION_ERROR.value
+ }
+
+ zm_client.login.side_effect = None
+ zm_client.login.return_value = False
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ conf_data,
+ )
+ assert result
+ assert result["type"] == "form"
+ assert result["errors"] == {
+ "base": ClientAvailabilityResult.ERROR_AUTH_FAIL.value
+ }
+
+ zm_client.login.return_value = True
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ conf_data,
+ )
+ assert result
+ assert result["type"] == "create_entry"
+ assert result["data"] == conf_data
diff --git a/tests/components/zoneminder/test_init.py b/tests/components/zoneminder/test_init.py
new file mode 100644
index 00000000000000..333106946bdcec
--- /dev/null
+++ b/tests/components/zoneminder/test_init.py
@@ -0,0 +1,122 @@
+"""Tests for init functions."""
+from datetime import timedelta
+
+from zoneminder.zm import ZoneMinder
+
+from homeassistant import config_entries
+from homeassistant.components.zoneminder import const
+from homeassistant.components.zoneminder.common import is_client_in_data
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.const import (
+ ATTR_ID,
+ ATTR_NAME,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_SOURCE,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.async_mock import MagicMock, patch
+from tests.common import async_fire_time_changed
+
+
+async def test_no_yaml_config(hass: HomeAssistant) -> None:
+ """Test empty yaml config."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.get_monitors.return_value = []
+
+ zoneminder_mock.return_value = zm_client
+
+ hass_config = {const.DOMAIN: []}
+ await async_setup_component(hass, const.DOMAIN, hass_config)
+ await hass.async_block_till_done()
+ assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
+
+
+async def test_yaml_config_import(hass: HomeAssistant) -> None:
+ """Test yaml config import."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.get_monitors.return_value = []
+
+ zoneminder_mock.return_value = zm_client
+
+ hass_config = {const.DOMAIN: [{CONF_HOST: "host1"}]}
+ await async_setup_component(hass, const.DOMAIN, hass_config)
+ await hass.async_block_till_done()
+ assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
+
+
+async def test_load_call_service_and_unload(hass: HomeAssistant) -> None:
+ """Test config entry load/unload and calling of service."""
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.side_effect = [True, True, False, True]
+ zm_client.get_monitors.return_value = []
+ zm_client.is_available.return_value = True
+
+ zoneminder_mock.return_value = zm_client
+
+ await hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={CONF_SOURCE: config_entries.SOURCE_USER},
+ data={
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+ await hass.async_block_till_done()
+
+ config_entry = next(iter(hass.config_entries.async_entries(const.DOMAIN)), None)
+ assert config_entry
+
+ assert config_entry.state == ENTRY_STATE_SETUP_RETRY
+ assert not is_client_in_data(hass, "host1")
+
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert config_entry.state == ENTRY_STATE_LOADED
+ assert is_client_in_data(hass, "host1")
+
+ assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
+
+ await hass.services.async_call(
+ const.DOMAIN,
+ const.SERVICE_SET_RUN_STATE,
+ {ATTR_ID: "host1", ATTR_NAME: "away"},
+ )
+ await hass.async_block_till_done()
+ zm_client.set_active_state.assert_called_with("away")
+
+ await config_entry.async_unload(hass)
+ await hass.async_block_till_done()
+ assert config_entry.state == ENTRY_STATE_NOT_LOADED
+ assert not is_client_in_data(hass, "host1")
+ assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
diff --git a/tests/components/zoneminder/test_sensor.py b/tests/components/zoneminder/test_sensor.py
new file mode 100644
index 00000000000000..0b9db89938753a
--- /dev/null
+++ b/tests/components/zoneminder/test_sensor.py
@@ -0,0 +1,167 @@
+"""Binary sensor tests."""
+from zoneminder.monitor import Monitor, MonitorState, TimePeriod
+from zoneminder.zm import ZoneMinder
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.zoneminder import const
+from homeassistant.components.zoneminder.sensor import CONF_INCLUDE_ARCHIVED
+from homeassistant.config import async_process_ha_core_config
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_MONITORED_CONDITIONS,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_PLATFORM,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import MagicMock, patch
+from tests.common import MockConfigEntry
+
+
+async def test_async_setup_entry(hass: HomeAssistant) -> None:
+ """Test setup of sensor entities."""
+
+ def _get_events(monitor_id: int, time_period: TimePeriod, include_archived: bool):
+ enum_list = [name for name in dir(TimePeriod) if not name.startswith("_")]
+ tp_index = enum_list.index(time_period.name)
+ return (100 * monitor_id) + (tp_index * 10) + include_archived
+
+ def _monitor1_get_events(time_period: TimePeriod, include_archived: bool):
+ return _get_events(1, time_period, include_archived)
+
+ def _monitor2_get_events(time_period: TimePeriod, include_archived: bool):
+ return _get_events(2, time_period, include_archived)
+
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ monitor1 = MagicMock(spec=Monitor)
+ monitor1.name = "monitor1"
+ monitor1.mjpeg_image_url = "mjpeg_image_url1"
+ monitor1.still_image_url = "still_image_url1"
+ monitor1.is_recording = True
+ monitor1.is_available = True
+ monitor1.function = MonitorState.MONITOR
+ monitor1.get_events.side_effect = _monitor1_get_events
+
+ monitor2 = MagicMock(spec=Monitor)
+ monitor2.name = "monitor2"
+ monitor2.mjpeg_image_url = "mjpeg_image_url2"
+ monitor2.still_image_url = "still_image_url2"
+ monitor2.is_recording = False
+ monitor2.is_available = False
+ monitor2.function = MonitorState.MODECT
+ monitor2.get_events.side_effect = _monitor2_get_events
+
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.get_monitors.return_value = [monitor1, monitor2]
+
+ zoneminder_mock.return_value = zm_client
+
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id="host1",
+ data={
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ hass_config = {
+ HASS_DOMAIN: {},
+ SENSOR_DOMAIN: [
+ {
+ CONF_PLATFORM: const.DOMAIN,
+ CONF_INCLUDE_ARCHIVED: True,
+ CONF_MONITORED_CONDITIONS: ["all", "day"],
+ }
+ ],
+ }
+
+ await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
+ await async_setup_component(hass, HASS_DOMAIN, hass_config)
+ await async_setup_component(hass, SENSOR_DOMAIN, hass_config)
+ await hass.async_block_till_done()
+ await async_setup_component(hass, const.DOMAIN, hass_config)
+ await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN,
+ "update_entity",
+ {ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN,
+ "update_entity",
+ {ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("sensor.monitor1_status").state
+ == MonitorState.MONITOR.value
+ )
+ assert hass.states.get("sensor.monitor1_events").state == "101"
+ assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
+ assert hass.states.get("sensor.monitor2_status").state == "unavailable"
+ assert hass.states.get("sensor.monitor2_events").state == "201"
+ assert hass.states.get("sensor.monitor2_events_last_day").state == "211"
+
+ monitor1.function = MonitorState.NONE
+ monitor2.function = MonitorState.NODECT
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN,
+ "update_entity",
+ {ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN,
+ "update_entity",
+ {ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("sensor.monitor1_status").state == MonitorState.NONE.value
+ )
+ assert hass.states.get("sensor.monitor1_events").state == "101"
+ assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
+ assert hass.states.get("sensor.monitor2_status").state == "unavailable"
+ assert hass.states.get("sensor.monitor2_events").state == "201"
+ assert hass.states.get("sensor.monitor2_events_last_day").state == "211"
diff --git a/tests/components/zoneminder/test_switch.py b/tests/components/zoneminder/test_switch.py
new file mode 100644
index 00000000000000..3665b2fa17eeea
--- /dev/null
+++ b/tests/components/zoneminder/test_switch.py
@@ -0,0 +1,126 @@
+"""Binary sensor tests."""
+from zoneminder.monitor import Monitor, MonitorState
+from zoneminder.zm import ZoneMinder
+
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.components.zoneminder import const
+from homeassistant.config import async_process_ha_core_config
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_COMMAND_OFF,
+ CONF_COMMAND_ON,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PATH,
+ CONF_PLATFORM,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+ STATE_OFF,
+ STATE_ON,
+)
+from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import MagicMock, patch
+from tests.common import MockConfigEntry
+
+
+async def test_async_setup_entry(hass: HomeAssistant) -> None:
+ """Test setup of sensor entities."""
+
+ with patch(
+ "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
+ ) as zoneminder_mock:
+ monitor1 = MagicMock(spec=Monitor)
+ monitor1.name = "monitor1"
+ monitor1.mjpeg_image_url = "mjpeg_image_url1"
+ monitor1.still_image_url = "still_image_url1"
+ monitor1.is_recording = True
+ monitor1.is_available = True
+ monitor1.function = MonitorState.MONITOR
+
+ monitor2 = MagicMock(spec=Monitor)
+ monitor2.name = "monitor2"
+ monitor2.mjpeg_image_url = "mjpeg_image_url2"
+ monitor2.still_image_url = "still_image_url2"
+ monitor2.is_recording = False
+ monitor2.is_available = False
+ monitor2.function = MonitorState.MODECT
+
+ zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
+ zm_client.get_zms_url.return_value = "http://host1/path_zms1"
+ zm_client.login.return_value = True
+ zm_client.get_monitors.return_value = [monitor1, monitor2]
+
+ zoneminder_mock.return_value = zm_client
+
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id="host1",
+ data={
+ CONF_HOST: "host1",
+ CONF_USERNAME: "username1",
+ CONF_PASSWORD: "password1",
+ CONF_PATH: "path1",
+ const.CONF_PATH_ZMS: "path_zms1",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ hass_config = {
+ HASS_DOMAIN: {},
+ SWITCH_DOMAIN: [
+ {
+ CONF_PLATFORM: const.DOMAIN,
+ CONF_COMMAND_ON: MonitorState.MONITOR.value,
+ CONF_COMMAND_OFF: MonitorState.MODECT.value,
+ },
+ {
+ CONF_PLATFORM: const.DOMAIN,
+ CONF_COMMAND_ON: MonitorState.MODECT.value,
+ CONF_COMMAND_OFF: MonitorState.MONITOR.value,
+ },
+ ],
+ }
+
+ await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
+ await async_setup_component(hass, HASS_DOMAIN, hass_config)
+ await async_setup_component(hass, SWITCH_DOMAIN, hass_config)
+ await hass.async_block_till_done()
+ await async_setup_component(hass, const.DOMAIN, hass_config)
+ await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state"}
+ )
+ await hass.services.async_call(
+ SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("switch.monitor1_state").state == STATE_ON
+ assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF
+
+ await hass.services.async_call(
+ SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state"}
+ )
+ await hass.services.async_call(
+ SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("switch.monitor1_state").state == STATE_OFF
+ assert hass.states.get("switch.monitor1_state_2").state == STATE_ON
+
+ monitor1.function = MonitorState.NONE
+ monitor2.function = MonitorState.NODECT
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state"}
+ )
+ await hass.services.async_call(
+ HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("switch.monitor1_state").state == STATE_OFF
+ assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF
diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py
index 50edcfec1571ad..146347620bdedb 100644
--- a/tests/components/zwave/conftest.py
+++ b/tests/components/zwave/conftest.py
@@ -1,8 +1,10 @@
"""Fixtures for Z-Wave tests."""
import pytest
-from tests.async_mock import MagicMock, patch
-from tests.mock.zwave import MockNetwork, MockOption
+from homeassistant.components.zwave import const
+
+from tests.async_mock import AsyncMock, MagicMock, patch
+from tests.mock.zwave import MockNetwork, MockNode, MockOption, MockValue
@pytest.fixture
@@ -24,3 +26,53 @@ def mock_openzwave():
},
):
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_init.py b/tests/components/zwave/test_init.py
index 4cf639e7faf672..16e8494963de87 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -22,7 +22,7 @@
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.setup import setup_component
-from tests.async_mock import AsyncMock, MagicMock, patch
+from tests.async_mock import MagicMock, patch
from tests.common import async_fire_time_changed, get_test_home_assistant, mock_registry
from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue
@@ -817,363 +817,497 @@ def listener(event):
assert len(events) == 1
-class TestZWaveDeviceEntityValues(unittest.TestCase):
- """Tests for the ZWaveDeviceEntityValues helper."""
+async def test_entity_discovery(
+ hass, mock_discovery, mock_import_module, mock_values, mock_openzwave
+):
+ """Test the creation of a new entity."""
+ (node, value_class, mock_schema) = mock_values
- @pytest.fixture(autouse=True)
- def set_mock_openzwave(self, mock_openzwave):
- """Use the mock_openzwave fixture for this class."""
- self.mock_openzwave = mock_openzwave
+ registry = mock_registry(hass)
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
- self.hass.start()
- self.registry = mock_registry(self.hass)
+ mock_receivers = []
- setup_component(self.hass, "zwave", {"zwave": {}})
- self.hass.block_till_done()
+ def mock_connect(receiver, signal, *args, **kwargs):
+ if signal == MockNetwork.SIGNAL_VALUE_ADDED:
+ mock_receivers.append(receiver)
- self.node = MockNode()
- self.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,
- },
- },
- }
- self.primary = MockValue(
- command_class="mock_primary_class", node=self.node, value_id=1000
- )
- self.secondary = MockValue(command_class="mock_secondary_class", node=self.node)
- self.duplicate_secondary = MockValue(
- command_class="mock_secondary_class", node=self.node
- )
- self.optional = MockValue(command_class="mock_optional_class", node=self.node)
- self.no_match_value = MockValue(command_class="mock_bad_class", node=self.node)
+ with patch("pydispatch.dispatcher.connect", new=mock_connect):
+ await async_setup_component(hass, "zwave", {"zwave": {}})
+ await hass.async_block_till_done()
- self.entity_id = "mock_component.mock_node_mock_value"
- self.zwave_config = {"zwave": {}}
- self.device_config = {self.entity_id: {}}
- self.addCleanup(self.tear_down_cleanup)
+ assert len(mock_receivers) == 1
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
+ entity_id = "mock_component.mock_node_mock_value"
+ zwave_config = {"zwave": {}}
+ device_config = {entity_id: {}}
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_discovery(self, discovery, import_module):
- """Test the creation of a new entity."""
- discovery.async_load_platform = AsyncMock(return_value=None)
- mock_platform = MagicMock()
- import_module.return_value = mock_platform
- mock_device = MagicMock()
- mock_device.name = "test_device"
- mock_platform.get_device.return_value = mock_device
+ with patch.object(zwave, "discovery", mock_discovery):
values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ 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 self.primary
- assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
- [self.primary, None, None], key=lambda a: id(a)
- )
- assert not discovery.async_load_platform.called
+ assert values.primary is value_class.primary
+ assert len(list(values)) == 3
+ assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ [value_class.primary, None, None], key=lambda a: id(a)
+ )
- values.check_value(self.secondary)
- self.hass.block_till_done()
+ 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 values.secondary is self.secondary
- assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
- [self.primary, self.secondary, None], key=lambda a: id(a)
- )
+ assert mock_discovery.async_load_platform.called
+ assert len(mock_discovery.async_load_platform.mock_calls) == 1
- assert discovery.async_load_platform.called
- assert len(discovery.async_load_platform.mock_calls) == 1
- args = discovery.async_load_platform.mock_calls[0][1]
- assert args[0] == self.hass
+ 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_device.unique_id}
- assert args[4] == self.zwave_config
+ assert args[3] == {
+ const.DISCOVERY_DEVICE: mock_import_module().get_device().unique_id
+ }
+ assert args[4] == zwave_config
- discovery.async_load_platform.reset_mock()
- values.check_value(self.optional)
- values.check_value(self.duplicate_secondary)
- values.check_value(self.no_match_value)
- self.hass.block_till_done()
+ assert values.secondary is value_class.secondary
+ assert len(list(values)) == 3
+ assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ [value_class.primary, value_class.secondary, None], key=lambda a: id(a)
+ )
- assert values.optional is self.optional
- assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
- [self.primary, self.secondary, self.optional], key=lambda a: id(a)
- )
- assert not discovery.async_load_platform.called
-
- 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
-
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_existing_values(self, discovery, import_module):
- """Test the loading of already discovered values."""
- discovery.async_load_platform = AsyncMock(return_value=None)
- mock_platform = MagicMock()
- import_module.return_value = mock_platform
- mock_device = MagicMock()
- mock_device.name = "test_device"
- mock_platform.get_device.return_value = mock_device
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
- self.optional.value_id: self.optional,
- self.no_match_value.value_id: self.no_match_value,
- }
+ mock_discovery.async_load_platform.reset_mock()
+ with patch.object(zwave, "discovery", mock_discovery):
+ 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()
- values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
- )
- self.hass.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(list(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
+):
+ """Test the loading of already discovered values."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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,
+ }
- assert values.primary is self.primary
- assert values.secondary is self.secondary
- assert values.optional is self.optional
- assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
- [self.primary, self.secondary, self.optional], key=lambda a: id(a)
+ 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 discovery.async_load_platform.called
- assert len(discovery.async_load_platform.mock_calls) == 1
- args = discovery.async_load_platform.mock_calls[0][1]
- assert args[0] == self.hass
+ 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_device.unique_id}
- assert args[4] == self.zwave_config
- assert not self.primary.enable_poll.called
-
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_node_schema_mismatch(self, discovery, import_module):
- """Test node schema mismatch."""
- self.node.generic = "no_match"
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
+ assert args[3] == {
+ const.DISCOVERY_DEVICE: mock_import_module().get_device().unique_id
}
- self.mock_schema[const.DISC_GENERIC_DEVICE_CLASS] = ["generic_match"]
+ 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(list(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_values, mock_openzwave):
+ """Test node schema mismatch."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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):
values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
values._check_entity_ready()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
- assert not discovery.async_load_platform.called
-
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_workaround_component(self, discovery, import_module):
- """Test component workaround."""
- discovery.async_load_platform = AsyncMock(return_value=None)
- mock_platform = MagicMock()
- import_module.return_value = mock_platform
- mock_device = MagicMock()
- mock_device.name = "test_device"
- mock_platform.get_device.return_value = mock_device
- self.node.manufacturer_id = "010f"
- self.node.product_type = "0b00"
- self.primary.command_class = const.COMMAND_CLASS_SENSOR_ALARM
- self.entity_id = "binary_sensor.mock_node_mock_value"
- self.device_config = {self.entity_id: {}}
-
- self.mock_schema = {
- const.DISC_COMPONENT: "mock_component",
- const.DISC_VALUES: {
- const.DISC_PRIMARY: {
- const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY]
- }
- },
- }
+ assert not mock_discovery.async_load_platform.called
- with patch.object(zwave, "async_dispatcher_send") as mock_dispatch_send:
- values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
- )
- values._check_entity_ready()
- self.hass.block_till_done()
+async def test_entity_workaround_component(
+ hass, mock_discovery, mock_import_module, mock_values, mock_openzwave
+):
+ """Test component workaround."""
+ (node, value_class, mock_schema) = mock_values
- 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"
-
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_workaround_ignore(self, discovery, import_module):
- """Test ignore workaround."""
- self.node.manufacturer_id = "010f"
- self.node.product_type = "0301"
- self.primary.command_class = const.COMMAND_CLASS_SWITCH_BINARY
-
- self.mock_schema = {
- const.DISC_COMPONENT: "mock_component",
- const.DISC_VALUES: {
- const.DISC_PRIMARY: {
- const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY]
- }
- },
- }
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
values._check_entity_ready()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
- assert not discovery.async_load_platform.called
+ 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"
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_config_ignore(self, discovery, import_module):
- """Test ignore config."""
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
- }
- self.device_config = {self.entity_id: {zwave.CONF_IGNORED: True}}
+
+async def test_entity_workaround_ignore(
+ hass, mock_discovery, mock_values, mock_openzwave
+):
+ """Test ignore workaround."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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):
values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
values._check_entity_ready()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
- assert not discovery.async_load_platform.called
+ assert not mock_discovery.async_load_platform.called
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_config_ignore_with_registry(self, discovery, import_module):
- """Test ignore config.
- The case when the device is in entity registry.
- """
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
- }
- self.device_config = {"mock_component.registry_id": {zwave.CONF_IGNORED: True}}
- with patch.object(self.registry, "async_schedule_save"):
- self.registry.async_get_or_create(
- "mock_component",
- zwave.DOMAIN,
- "567-1000",
- suggested_object_id="registry_id",
- )
+async def test_entity_config_ignore(hass, mock_discovery, mock_values, mock_openzwave):
+ """Test ignore config."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+ 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()
+
+ 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):
+ 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_values, mock_openzwave
+):
+ """Test ignore config.
+
+ The case when the device is in entity registry.
+ """
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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):
zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
- self.hass.block_till_done()
+ await hass.async_block_till_done()
- assert not discovery.async_load_platform.called
+ assert not mock_discovery.async_load_platform.called
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_entity_platform_ignore(self, discovery, import_module):
- """Test platform ignore device."""
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
- }
- platform = MagicMock()
- import_module.return_value = platform
- platform.get_device.return_value = None
+
+async def test_entity_platform_ignore(
+ hass, mock_discovery, mock_values, mock_openzwave
+):
+ """Test platform ignore device."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
- self.hass.block_till_done()
+ await hass.async_block_till_done()
- assert not discovery.async_load_platform.called
-
- @patch.object(zwave, "import_module")
- @patch.object(zwave, "discovery")
- def test_config_polling_intensity(self, discovery, import_module):
- """Test polling intensity."""
- mock_platform = MagicMock()
- import_module.return_value = mock_platform
- mock_device = MagicMock()
- mock_device.name = "test_device"
- mock_platform.get_device.return_value = mock_device
- self.node.values = {
- self.primary.value_id: self.primary,
- self.secondary.value_id: self.secondary,
- }
- self.device_config = {self.entity_id: {zwave.CONF_POLLING_INTENSITY: 123}}
+ assert not mock_discovery.async_load_platform.called
+
+
+async def test_config_polling_intensity(
+ hass, mock_discovery, mock_import_module, mock_values, mock_openzwave
+):
+ """Test polling intensity."""
+ (node, value_class, mock_schema) = mock_values
+
+ registry = mock_registry(hass)
+
+ 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()
+
+ 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=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry,
+ hass=hass,
+ schema=mock_schema,
+ primary_value=value_class.primary,
+ zwave_config=zwave_config,
+ device_config=device_config,
+ registry=registry,
)
values._check_entity_ready()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+
+ assert mock_discovery.async_load_platform.called
- assert discovery.async_load_platform.called
- assert self.primary.enable_poll.called
- assert len(self.primary.enable_poll.mock_calls) == 1
- assert self.primary.enable_poll.mock_calls[0][1][0] == 123
+ 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
class TestZwave(unittest.TestCase):
diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py
index 8306899ce02e1e..29c1126b5d139f 100644
--- a/tests/components/zwave/test_node_entity.py
+++ b/tests/components/zwave/test_node_entity.py
@@ -1,8 +1,4 @@
"""Test Z-Wave node entity."""
-import unittest
-
-import pytest
-
from homeassistant.components.zwave import const, node_entity
from homeassistant.const import ATTR_ENTITY_ID
@@ -235,436 +231,492 @@ def listener(event):
)
-@pytest.mark.usefixtures("mock_openzwave")
-class TestZWaveNodeEntity(unittest.TestCase):
- """Class to test ZWaveNodeEntity."""
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.zwave_network = MagicMock()
- self.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,
- )
- self.entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network)
-
- def test_network_node_changed_from_value(self):
- """Test for network_node_changed."""
- value = mock_zwave.MockValue(node=self.node)
- with patch.object(self.entity, "maybe_schedule_update") as mock:
- mock_zwave.value_changed(value)
- mock.assert_called_once_with()
-
- def test_network_node_changed_from_node(self):
- """Test for network_node_changed."""
- with patch.object(self.entity, "maybe_schedule_update") as mock:
- mock_zwave.node_changed(self.node)
- mock.assert_called_once_with()
-
- def test_network_node_changed_from_another_node(self):
- """Test for network_node_changed."""
- with patch.object(self.entity, "maybe_schedule_update") as mock:
- node = mock_zwave.MockNode(node_id=1024)
- mock_zwave.node_changed(node)
- assert not mock.called
-
- def test_network_node_changed_from_notification(self):
- """Test for network_node_changed."""
- with patch.object(self.entity, "maybe_schedule_update") as mock:
- mock_zwave.notification(node_id=self.node.node_id)
- mock.assert_called_once_with()
-
- def test_network_node_changed_from_another_notification(self):
- """Test for network_node_changed."""
- with patch.object(self.entity, "maybe_schedule_update") as mock:
- mock_zwave.notification(node_id=1024)
- assert not mock.called
-
- def test_node_changed(self):
- """Test node_changed function."""
- self.maxDiff = None
- assert {
- "node_id": self.node.node_id,
- "node_name": "Mock Node",
- "manufacturer_name": "Test Manufacturer",
- "product_name": "Test Product",
- } == self.entity.device_state_attributes
-
- self.node.get_values.return_value = {1: mock_zwave.MockValue(data=1800)}
- self.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 ",
- }
- self.entity.node_changed()
- assert {
- "node_id": self.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 ",
- } == self.entity.device_state_attributes
-
- self.node.can_wake_up_value = False
- self.entity.node_changed()
-
- assert "wake_up_interval" not in self.entity.device_state_attributes
-
- def test_name(self):
- """Test name property."""
- assert self.entity.name == "Mock Node"
-
- def test_state_before_update(self):
- """Test state before update was called."""
- assert self.entity.state is None
-
- def test_state_not_ready(self):
- """Test state property."""
- self.node.is_ready = False
- self.entity.node_changed()
- assert self.entity.state == "initializing"
-
- self.node.is_failed = True
- self.node.query_stage = "Complete"
- self.entity.node_changed()
- assert self.entity.state == "dead"
-
- self.node.is_failed = False
- self.node.is_awake = False
- self.entity.node_changed()
- assert self.entity.state == "sleeping"
-
- def test_state_ready(self):
- """Test state property."""
- self.node.query_stage = "Complete"
- self.node.is_ready = True
- self.entity.node_changed()
- assert self.entity.state == "ready"
-
- self.node.is_failed = True
- self.entity.node_changed()
- assert self.entity.state == "dead"
-
- self.node.is_failed = False
- self.node.is_awake = False
- self.entity.node_changed()
- assert self.entity.state == "sleeping"
-
- def test_not_polled(self):
- """Test should_poll property."""
- assert not self.entity.should_poll
-
- def test_unique_id(self):
- """Test unique_id."""
- assert self.entity.unique_id == "node-567"
-
- def test_unique_id_missing_data(self):
- """Test unique_id."""
- self.node.manufacturer_name = None
- self.node.name = None
- entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network)
-
- assert entity.unique_id is None
+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.device_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.device_state_attributes
+
+ node.can_wake_up_value = False
+ entity.node_changed()
+
+ assert "wake_up_interval" not in entity.device_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/conftest.py b/tests/conftest.py
index 9008359e539243..25572a2269be9d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -357,10 +357,12 @@ def _async_fire_mqtt_message(topic, payload, qos, retain):
return FakeInfo(mid)
def _subscribe(topic, qos=0):
+ mid = get_mid()
mock_client.on_subscribe(0, 0, mid)
return (0, mid)
def _unsubscribe(topic):
+ mid = get_mid()
mock_client.on_unsubscribe(0, 0, mid)
return (0, mid)
@@ -395,6 +397,13 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config):
return component
+@pytest.fixture
+def mock_zeroconf():
+ """Mock zeroconf."""
+ with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
+ yield mock_zc.return_value
+
+
@pytest.fixture
def legacy_patchable_time():
"""Allow time to be patchable by using event listeners instead of asyncio loop."""
diff --git a/tests/fixtures/netatmo/events.txt b/tests/fixtures/netatmo/events.txt
new file mode 100644
index 00000000000000..f2bc29f782c1be
--- /dev/null
+++ b/tests/fixtures/netatmo/events.txt
@@ -0,0 +1,61 @@
+{
+ "12:34:56:78:90:ab": {
+ 1599152672: {
+ "id": "12345",
+ "type": "person",
+ "time": 1599152672,
+ "camera_id": "12:34:56:78:90:ab",
+ "snapshot": {
+ "url": "https://netatmocameraimage",
+ },
+ "video_id": "98765",
+ "video_status": "available",
+ "message": "Paulus seen",
+ "media_url": "http:///files/high/index.m3u8",
+ },
+ 1599152673: {
+ "id": "12346",
+ "type": "person",
+ "time": 1599152673,
+ "camera_id": "12:34:56:78:90:ab",
+ "snapshot": {
+ "url": "https://netatmocameraimage",
+ },
+ "message": "Tobias seen",
+ },
+ 1599152674: {
+ "id": "12347",
+ "type": "outdoor",
+ "time": 1599152674,
+ "camera_id": "12:34:56:78:90:ac",
+ "snapshot": {
+ "url": "https://netatmocameraimage",
+ },
+ "video_id": "98766",
+ "video_status": "available",
+ "event_list": [
+ {
+ "type": "vehicle",
+ "time": 1599152674,
+ "id": "12347-0",
+ "offset": 0,
+ "message": "Vehicle detected",
+ "snapshot": {
+ "url": "https://netatmocameraimage",
+ },
+ },
+ {
+ "type": "human",
+ "time": 1599152674,
+ "id": "12347-1",
+ "offset": 8,
+ "message": "Person detected",
+ "snapshot": {
+ "url": "https://netatmocameraimage",
+ },
+ },
+ ],
+ "media_url": "http:///files/high/index.m3u8",
+ },
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/ozw/climate_network_dump.csv b/tests/fixtures/ozw/climate_network_dump.csv
index 370edc15be1afd..99cef9091c57f7 100644
--- a/tests/fixtures/ozw/climate_network_dump.csv
+++ b/tests/fixtures/ozw/climate_network_dump.csv
@@ -173,4 +173,36 @@ OpenZWave/1/node/16/instance/1/commandclass/113/value/72057594312409105/,{ "L
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}
\ No newline at end of file
+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": "iVBORw0KGgoAAAANSUhEUgAAAMIAAADICAIAAAA1GKkAAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nKy9W69lyZEe9kVkrr33uVZ13bq62U12k00O2ZzhWMOhJI4kA+MLBMOCYRgQbAgw9AcEPRr2g/6EDRgY69EQIAOGMXrxBYIxtmbkGcm6kKKHIqd5bbLZtzpV574va2WEHyIiM9c+RUIGvGfYdc4+a+XKjIz44ovIyFykqvjlH1UlovozoETYbrfPz87OPn22Xa8VyDlTTouUlSCqADIzE4MAEDEl4pwzJQYREeWch5QXwwAmkF1ERMSUoWB7GgME+5M9nZj9yvhSVaEEEDGIQOSt3e15fASAeuvoL7Xv71z/yz5kd5vo/o3val16Wd9+1fX/P34UgGrt9P+nR2j8E1PS/kQmC2uu/+9+E6oAtpv1Bx/8/PLyEqqkPv0AlCmBiHmxWq5Wq8y8XW/HcccpLZbLnNI0TdvttkCXwyIxl1LGcVRVSkzMap8CcpVSVS0qNrW9oitAADMTkYiYGilEVVRVRETE+s/MqmpXJmJiUjIzSETMzExkf2XmJg7/G3NKidnELdGBxJxSSmnIechDthsJv2oaNITvIlWfgDoov/1lbdydiH+Tb35VZ+yhZrq/8rJfpgB9nxXtsv1JiuvanQQF9OLi8sXZ2fXVVRGhMEci2CwQ85CImFUBAkVvidh0V0QMLRIlFS1lBMCJU0pKmKYipaSUhyETkSkZMw3DgpmnUqZxBCHnlNKgIpvtlpkTUx4WKrobd1JKSinlTMA4lTIVZgyLgUDjNMk0MTHnRImL6DROgA45MbGqjmNRLTzkxKyqpQhEmDnlTERFpZTi6gWIK6sCUBGBQhTMCQQysCQQiImIoBCAwEQEEuaUiMBMRICqqgJmjWAikGl1zlmkiIjblKs6JbZBJwBgtekhu9mwmOe6Sj1qkPcPAabzeVcFOabHIBgAmEhVRVFKUdWUOKXExD7Dv0qNum9E5OL8+Ycffpg5jbudaQMRE7GqqS0AzTmJFECZGSAmNnAgIk6JiFV1mkYAifKwGEopZZqIwIlTzgDGcRSRnHNKKaU07sapTCklZrbJLt5+IkpEVMqoqiZ3QyYRIQUxEydmllIIWqSAKHEyF1hURdRmgxQiMhZJKaXk4pimybqRUwJIodvdLjHnYUjMIrLZblV1GIacMwFTKbvtNuW8XCxSStM4TdOowDAMwzAosN1tx3EahuFgtWKm7WY7TiMRL5fLnPM0TbvdFophsRiGpKq77ajQlHJKyfpTpIDZ5KKq0ygAMoOYiromm4JoIEjvYQC36hk+EEihqkqO9wojLKEW5ArOnTewDqSUFsvlwcHhwcFhVda5GjnoASq73Xh1eXl9c725Wa/X6yHnXr/92XEvM1RVoMwEdSXz3jIIpKqlTIAyMacEUVExEzDPMhVRFeYEIOckpVSVZWZARYoZX06DqoqUOjZASynGbpjCS4q6PTGriIoo3N6hKiLERHC6ZjZgvAs+HUpuc/3H/qLMxCmZU5umybxnSslcq4qCiBMrbKBKRAZ17nMBTkxEqobTSMzEJCKlqEmAwKoyTQIoJxMCisg4TeZQiTCVUkQISJyISYFSrDMppUSEcRIpExOGnIiSiI7TpCopp0XOCt2N4ySSmXPKRDxNZZwmlUp/6pQLc7LZMQe0WCxPTk6Ojo44J6rMo1em7XZzeXF+c3MzjuN2u0WxqZopUa/ycAZASsjDQpVub9YiwsxkFDubxI0PU9VVAlJKxFSKjGVy30FkeD97nNFpc5MgMR0lAiExqRTVgsqsgjYRw6SvpagKiDklAlRKKYJAMgChRhxcT0TFYN2wUNRcDKeUoKIiRqPMPVnPZ5LR6iXA7sDUdFpEoOKUPrE9jWJ0RKmU4vwPDJCqiBRmJmYLQMo0MSilBCYiKlNRKcREnAAQqJTJ+gp7uLgBAyxQqEKRGGAypSZRVSVOIlqK9MQpxmUskwkkQBEBMAzDycnJw8ePiCj3tqYi0zTdXl9fX13ttrthsTg6OIT+Eh4XslFFAYB0u9mtxzJO5fZmU4qIihRJOS2Xi5TYdE2dVhikwnytqJZSRMR+RplMH+BQGy6dYBZTSsnDkpm269t7J4fHR6uUSEUigBKKeM46CCLm3MEtpZwUMOcLICWOMTlzZGIFGX4QgTnZra5wiLG4W0cfrHifQQpS0QKjmDYMMli0X+0RWpRY2TEeIhUOlYhEtYiIKgMMZqZSSlGIasqZGSIylYmEU3JRjWMBhFNKaWDmaSoihUDMyTCyjOOoQilxYua0242lFGJmTvv24PzMoo0EKFSgMk1lvb69ubk+ODg4Oj7OLlnRcRy32/V2s96uN5nS4vAIFNR4rjtNW00lCEV5PeLF1e7qZl3EZ2U3TrvdjpkX26JqrE6lSCigwb8wc04ZBE4ppTzudgRJzNrGYQQUIgqi7Xa33uzAi+3mdpmxXK5OT4ecFCpErCqqAkcrm1pR9XCsheiqBGXm4sGv/BIbqR/TM7VgUVXZBe0mq05Mqv6AlOw/2t0PIOchhOeQxZmJVM3HK1LKdRZVBURcbxEVKSklogSgiBYRIuQ8EBHgwGl81IZfSlEl5sTEIBKFqoCJkIlZlUoRURihrEFrtUAiSimJJVzUOGgp01Smcbteb8fdYrH80q/9Wga0TGXc7a4uLm9vr6dxFLERUe8gTfQW6oUvcJMXhYLOXlw9v7jZ7aaD4+M8LIiVF8thVUCUOYHAlEHKSimxiNYUhsK0hCwIWh0cp+wiMCZRAckgV3kruby43Lz/84+/+Vtfffz44cEyMYqFF6oqWhChh2gBkvs7jyHUCZAxM7IMVYUQv6LXJ4MGQ5z41pxSIwOhRuZ9NVxB6F1ktESE2d06x82EMGWAteKidvxYa7aMXCFb3gcRwmsj2z5a6yEn9u+UzHcby1EogUEYBqpyoYjiiNTCRgDJ1dqUqEzTuNlsNpvNer352e5n73zxixnQ6+uLq/PLze26lNIEBxiNh3Q0pepqtTyoAhPpxdXtZhRRbHc7BGqbYUX8y0UnJZZm+yQwSDINwHa7Xa0Wqh7kirMMYuIh50wERVospuvtvUen+eMPHz68v0ykZRSoucCqCoYWxMk1HxS4ED8RACRi9bmIyXaVA5uf4i7YcVWlmLN9Cn6XaNaPiGFkg3MHm15ZOxR8aSOt/107ezkgsWg9qEjNNfoPSjU6I0BIHBeAmoWLYCMPw1BBXUKFjDFvt9vNen19fQNeT+OYpYwXz59vbjdOWuz/VC1rbA933AhBi2qELAAUFvqobjbrV165v1gOyfNrChHjtaqiUjgxylQUZIZAtMhJkaVIERnH6XA1TLIbd0YRijF0E9nWaTXWu1EobcfCRLvddrvlTOretksFspI0IZP5IjON9mUzZ5vDpiuRq6XqzZqGaQBfNahGRf2/DSLQ8u8w5e5+7a4nkxlVJkjVIvbV1H9w4XecrFO9WQBUAdUZqX8bk6nU53gjfOGcVBVg6VK70zTtdrvNZnN7c3t1dX15fZXyAkAeN+NmsytFENK2oVdFIQ+DSzyEiFgdsAGQKkSFCF/54jsPHpymZOG+D6b20rCCAQ+fqqxDaqrqsR0TgUVLIjbZeycUzDxBBfyjnz3/9IMfQEoIxdtSqEJIAbDBdPy1yZdc/hbPq0AhIPfjZFEAqcdYNRtDCkXpokygUni4MyAwITLXwcPCc7nXMF9nWl1VqYmoKjiCdsW98WRi5spKozdNKZ36WeIzqEmvtdY3J/zEhtvm5i0UJkpMlIacc95O4/X1tZRpmsZxLNvtdrvZbm7X19dXF5cXN7e3J6f3mTmfn5+LSIzSvJTWITMzlAx7rRNSxVQVBQDR1Yvz8/PLjz9aLBfDkBJHRrXntv1I9gcGCwjsv76OwQATwAxO5AaqzAlpOSR+eP9+SlQEnGz2yE2USOHxkaoNISg9jD/YtAkAJalzjDDUYCtNG9ovGobUNDMmnaC+btIIu3a3W9hlqAlzNz3r94+jXyVeXdDkWqGq7nBpRrz6DxGBxGSy30jXtx7zwvvA0+FEecjL5fLk8Ojq6qqUstvtttvt7e3t1dXVxcXl9fX1NPnqUx6G5BEXGeihIxiWL4lOQAS+4hbegwGQiqpsdttJSGUaF2mRh8Sm5MrkaWj1rgcfnA+JnP81nmscRG1yBar2cGXmSfOm8Pn5C3nzoc8+vIcxsY4BaisW/i27pfeTrQSjnxSaFDqjDT4C0gykVZTEYLjXAFeLugRXZyhSa66sVOcrNOMuyYJ6HGdZxd7vumOCiInf9bpCmdF6JtZemUUMtLSyQggAgTJIjCYFOobnQSnTuN0Nw7BYLq6urrbb9Xp9e3V1eXF5eXN7O41SHXHuEL869DsfqjbZ241HKyBGQREwZ8t1WmLaQ1/MVFNVwnsENZk3CIlmlSoJC3GbfzTGp9M0kQcWcUslDRGWVXIQahJDUAUsaqf66M6rVGLTpKMdCgeK+BBU1SauOqKwmR62PYoDXG2sR3s4HcDv7Qff7lqBEpGIgllV0Ct/d2EsUXMkQaw1iYloDVcjc7YlDThoUoHsVJWQUtrtdpeXlxeXF7fr28lW1sMj5woTvX+BZRdmX6Iqf8e5m3apaorFcWK22Ixi7BUIfGUxbqSIV9U9efxkwM+9hml0jIjIGzSRwJNbHhxDVEkUZDnxueqbOlBITixXSUqVvXgjLus9pxz3de1ZAr1ym1/96TviiWtEvFaTTxHPdFeHvZHFhxyS0Qq8NbcAcv1zN+xZNHN/IFhChMIXqxMzj0HdDMMeRIVFdtstEe222+urq/XtehzHcFOW26YsUkNHnfuaJhQRIeL+r120CdGwKl9odp4zV8vgH5b211CkbtU5nhq6u4eLZjwRu8azqP+z/UvsQdTerM5YiJpNS3Ux2g2/J3CdxXRGFUQpGHCA357Kds9F326T5MtFanjTt9Td2lMlAFAyVuLL5NFIYwdtSh2kXXx1RJ7G6/1g5cG2+gsSkc16s1lvbBHd+o6YzNzwMCib84UwR0J1zzMR168cEFzVXvohG6x3PX5GjInbgGNRmmnmRfy5XKHTqUcEZ1rjxgApxsyJNP3w2aLeK92de9QZqXYdAgiVnZGBX/ZRrbKu4ctM1agRhhn0znWo67P3VustBOpufWlntPsT1eqMvVFUza7tO/8A2fyO4zRNpXHNwDIQcuUQRPDVxFgnqngBstKwhpm9nIi4ybr6hGYWEZHOMhNhSRq9mX/E5Nh4AYWjM41RAF57Vrmndza8WBDbGSWKPsN9ehCC7vkNigCoUbV2X8xG5Im19m1fP7oGEZZZ21Hq1Kde2Es1BkV7IRt1fyWwRwfkqZm7sVj9McZN/SVVdeKaXhqmLmyeXlVLKUVUZ806y+NOucwxSrNAM/aIQCq227qVhupoxNPV28wG6x3UflRxW1PJ5pw66hdrCZ2Uw60BII5Fmdlk2DANdQ0FnOSZKTueR5OE/Ynf47wUn+iC6mzZxIhEd000UvX3l3EmRdBz2r+9H3CLcV+mdtRV7Kje0UxUq68hjvbC7v6/XtqbZSX+EJEipaNx8OQaEdUVfvKIqzkH1HVsqCVJrOZNZ6uYBEDUnWgPP8HEqYnAPVrjPXsKRNVbV7prOezarIKIJJbYmA0/+852RQ5BwRCkJ1oFQJalMCglVPnGrbWuq/vGG+UW8cXHEgeh4Y3+UKdJLoTQFerVwkpxVe1H6USICCnnbjeizhnYqPvNucJVLFO3KoWqsYgG9fNJaNIwaGjjVBEtWlmwtn7nzil0WNKicVjtImEmPaqY0IKLWZeqdWmX4+rF0VTMZBAd0hoKuvw72jTrgDYcaj2jrhcaxLn7Y5dAmIuvIVOF2CrWmYbV9M8MJsM5dxfPPbXOnuWjpKoozWZnfdMu0T27u7XSsau7Ck6eke1dQaRhnDZUO+8zDg3B4hHucGwmfbSRyAWQQfVq9alxFZmFUNoE6p6uMjERQMnrArxLnj1tcKxWPcMOwIkAqFTC6CuGAAqUm+o00Vj81fiumhFLfUiIPrAsEjAxtT5GEFkdYCgNALUEuJd2AL23q7xEVdUj4kgjuLJQ14eXb4iovtHXsMDa7vGr68aBUOeqPf0cq2uHSLcTwRbuNTIy1o64AOtqTM+KTLINAqR7in9ZOsphTs0UkuA+wjTFxpXJ4l5EHsZJaVBag1quRQ11nE1S4YwAuCfQ8Aq77WacxoODw6mMViI4jiXnnBeDL9wCgBWqJzb98xpXJVUrkbkzK2TSokBW8ru8Mx0ph080uBqWWkpMzT+5uEmtTs1vkjlo9YAkDoT7OEi+UuZ2f6fP7vKdc0IQVRwIR2Az2KVz6jz4nNiEqel8420xox3wu9IHIM47o8Qt8oiW9M5l9qsGZNSZ7/9VVTXdsArrpGIPVHGPR50VQmSqjBOd/9onlVQLRwlQUbm+uby6vHzt1deu19fb2/XNZs05WWpgtVqWadpux5wXp6f3jo4OL85fHB0fH56cllI+/PnPpAgz3b9//+TklFPuQgGrSMw1veDLGUAsRHBVaQBQhhtWxGV2D4lbS0UBm1eqax71y/YgBldCphAKWAyJ70+G47c3Lbc3V2dnZ+N2pwCnWHImBjAMA4AEXx2llAgYFsPq8GgYlrWp1lHVpt59FpFIfW3P0KTnZ52Kqu21Mjj3jXue7ybyRRjHTYgX2cVf0S2zGhqVIsGkekq459dnyoNQ/JARPGzTyOfYPkaFiF5fX/1kvRaR+6+cqpbb2y0zr5ar0+MTAOvNJufFcrl4/vzs5vpysRpQRGSSUg4OVuv1ZrPZHBwcLlLe4ygI54oIv6pJuQRDgP28trW2xipinWSWpnJ1MvWo30cmuBMLAdHCfvSP9nSHhyIfvP+zi4sLCyNURKHMBoSedSuTlFJSSpwSE3FODx4+fvLk6bBYqmoteYtONgLX/L13knyRX/sMO9XkVfWwrXs689AIk6v4HgAD7YcGAMiqBTOujKrFWheQiWzDKaH5WvVNTB5LWBW9VKcWjHGxPHj8+NH6dm2Zp8ePH19cXC2Xq2FYikpOhZk3m835+TlBPvnk0+2mTDptt9thSCJlHMeplIW15sBAXtO5H2aTQUT45VrWGIJQ14QqTvySD3VRp7h3t3mb2feMN1e2FKKPqUZdEFXQzc3Ncrl49dWny9VSgc1m/eLF86uLC3OJAgx58fobn7N5LeP44uJ8vb4dx3FYLKLPGktjtT+IXysftWvZfHgHDjIPcjh0SBytPEnY+a5wrbbWZuZWH0RRwZFLKY1xzQCZFHVno7PJWZxVtdSJtogUEaty8tWO5XJ17/4rB4cnCh532+Xq4OT4dLU8uri42I2T1RQnzsvl6vHjx+v17TAMh8fHRcrV+cXN7XoapzwsHOgQBY42vlYwhIqjzg+CXgBis9uMaV+HKqbOzBp7KtbZMCyqpMoqGtrFNS1zYfYtlSCLqso4lvOLi8VmSUyb7Waz2TbmQZimQoSUMzOPAFsSxpJoiOpYM2zROjXRgbq8ph1valwtOFOYY0suxJy6e9TgWiCQOu+rkOaXE5oNZyvad3NpotHaR1NA8vjQW/LEA1GMTkGUkm3PT8Rs4H9weLxcHeacDo+O5fDgsBwvFgcHB4k4L5ZD4pSGhZX0D4vlwXaTU86LhYo+fPREVFRkdXCQhkWpVVqGdoJCShVvjI3FOMNVVZG+ZMdMT2Lsh7qGNVPWKBn0pbcKNJb0t9IW34iidUrq5KH5Ny81BEhVdtNoD9tsN+M05WF5fHRIwIsXL8ZxevHi3IqIp91ut90eHBx2RxrUaZr5jtCDzuH5gpFd3/rmFlFJgMsHne+qQ482tRa7ahUXgmzZrzm0rIdrV0OXbxWC0zqKR9YyBrIdQtM0jSNz8gIMtl0pzEWUOGXOKQ2qJKKHh4em8JkTfO2GV6tDwBnm/VceWCcsUx0aUWXBpShFiWan3nEVHKFUpU9boLtyP4quE09urkFDHMjd1gPxVZXI9ibUX5twe3Fb2lRjK+SwGO7dO10uDwDc3i6umYdhePXpU1LkYfns00+Pj4+IE1R3iW2rcReH+8Prc82K5qTRS7C1Ye1MOyoUOAKEAVQX3dGbpjc1DKwsvv9rU6NYLQhYJhfcLPCY9R1aZaxaStlsNsCkGIaUOCLPrkOkULbK1HkE6cOqYUiX6rbdsY6xAIFEUJTLWKxYwgy/kjittdVah0r1qn6mqf6irT+NilbBE7c1SieRtda4m5/ZXDb/WVtVp48iMu02WyiIqExTTvnk5PTk9J4qhsXy4vx8kXPKg6oy5PaGezVQ7ZW4oQKFs+0QYbZlql7fa1VlVUEZous+6z3v8+o/8uIMp+I1uZCb8rayB28vCIdnRKi3sD4YCbu1lVKO/SjUqbUZYkpsOZvosGpbfCH7Q9krZHMa2VbERGQSmkrN2gXiqyM5VDxyms1xjxadBqhUgWj9mzrWBlXqjaFTl1nURDXeIWo5IYStWSCWMk+78eriglMCsBt3OefTe/fKVADabnfb7fbZ2TPzMyLTbpwODk9mCoGKTgRy/e7JNbpU/gykm2urpUueO99LagAzRKoYZrvGgdIFrw2Navfu3onYGNFpZnCLepfCKhIJwNXF5S1LTlHBxkzMzATQpLJcLOx+jlLtmnhgdoxq5W4ROjdQrXuEJyGpBU0aNuq65EsrfcF7jKiKtFJI1Mlu2KlzmcYVGlFPtBR3UHW7tYVOiu25qqqiy9XqyZOni8VCRK6uLy/Oz58/+zTnRERnz86I6P69V6ZSAGw2t0XWXZuEoGYNGwOhuhkBAOIE35BeWX8ItZvMsC6N9rn+tY+oqtxMJjWpWIdctyT3iEeN83tHZiwVgAfCnnWGFBWRm+ur/+N//18/+eTDlHixWCxWy8Oj46Pjk8VquVgsh8WwXC6GYTEMw2IYcsqccvITROzcoFyrJ4lbxiV8UgRiRKXIJAkyQWN3oIvHjAwKJc+oMjXrpGA5Wp0s2CyxxaQt8DV7VQBR3QbMtLG240jZ6WmEdQDqjllVnSYllu12U72MqFxfvLi8fMHEROnw8Oj4+ISHzMDN9dU0TUR2pkWdI3MuKTyCfXmHemj9svKhqm2hl2FiqhztR4UhMJ9xUlXY6QNB3sVShMxElN1VKTWuNXNZiqh6t2brduyWPQAAjOP4ne9855/8kz+5vDjjzCBS0Guvv/Gld98dtiuhpL4+LLZ90WuumSw5VERKKbD96qWY5ZozY/iWEYOfnFLi4dGjp//2N7/JmIUXtceW/pgzT/XZdepdiVKdCcxsTysiu80H1LTSqYgBlUL+mOtSWIG7BFVNOU1jOTt7MU3TNE2qolqgRQFlVcLl5eV3vvunXsyumpiPjk8juOlUBNqxH/9DrffVYKwUUHNHy8JSYvxdp2vGpIJKQExXI1GV0PNGLiNQNWZVULchxKiOamkSdpikqPNVkIzjdrtdgzQlSjmJYhT59Pz50dnZ13/nryxWh9c3l8MwrG9uDw6ObHuusenlanXv5OTgcHV4eLg6PDw+PmYg+RYlF4NtHC8yTeN4dXn10c8+vHp+tZ0mqQLrHAq7y7GyF6HYEIbYE632132VqYBSDSqBQiI+E/1Ofu3vdYSrS1x1eQGWRAPUTg6hV1555cnTp8NioapXl5effvTh1dULIgMYvnfv3huf+xxTEi0vzs7Onj0rpbjvd/SxZIp2va4xVGkjIj8EzFYItQ24qmMnASICixTVErsAZtZJql4rPfsSTGSnzWQ7ysPwqdpgqIuSbXcIlfVvwj6ZWBVCMk0jEeXFYEtwxuDyYvjN3/7t/+iv/2f3Xn2dhoGgOecU3tf4RFFREVEpZRKonZNnTlSJFEJMEJ0mud1sbje3ZTeevzj/g3/8R//Jf/Afb6cpkj0RPgC9ydaIwUWuddb3jDP6Mw9n9kTZBUeV5lZYavEOXvoxNc4JhKurq8kFTmUct7sdcS7TmJKCsSvT1dWVoVGc1JMolu5b7qN2KrS/hdh91koLkVOUikm9oPbGaKec9WWydQA1UrOI3y0pbs+TTMQdj3aNobhMiGwxFWgmC3hMZNjGFb25Rfr09M03vvwbv3H68AFIpzISESkbZiYmZy6AMhGSHULCiVULgeygBIWAQJlSznk5HE/Hqvr48ZPPvv7Z5x+dZUAoot1w7OGEIpz3/tbjExorjK1MNkN2DglHnJXmUkKUuHjxST8H1Pad7ZWiByX3WBOLxeLk3r3z8xeXF5foKn1USYmVmJR2u90HH/yciIaUpIiopiFzStJ8iyC2SlaF1jZrCIdiHicHLakPpMbnZn3tXV+/OTH+06kUENlX32lqiyEUlqpApIsqLOksAq+oPvvE0R+w88tAJIrlwcHh8TGYigooqeo0TZkTM5dSIsEKkK9+EiU7o0hDvKAWaBJzGogUOQ2rxcGHP/94ubBjoLoaN+8E1w536EJ9b32Dzr5FalDsqHu5wynaF3vZy1Y8EqWgMccVwYjoM5/5zMOHD8pUbC+HfUTiCFQQyIWZQKqaF4vj01PLDjS5q0byMLaqEYcTsTnuuqn9rVWH6tCa89kPM51jomuqxzzXW+dGWoQ47eM89ZH2bAj2c+Qq/AIDPGYWwJf5mThnyrkopIgltavUmOomdAJMjnUvOar62rINez7GEBzEIKFxnHi1sKebsQd/cZQ0VJrjQytLUlRXrd0EzOXYldpok4JTEewxeAKAcbe5vr48PDxMOY27sZRSShnHabFYHaxWNzfXKVPOw8HqqEzTNBViOy0uyvOIUhqIqUyFncJxzgO5W2AngoqGgPZsXwnrMtGtUzMJ+A1+YgIHjGmDHlTP3pJvTLP90wG0Cj80ERlNISMrVUUWD3XN6zd3uLm5b7Y6FNvK6FNCrCADKTHb8dsU6kumiGdrzCrZLndfm64pHcAYnmmegBRTmexQwPi7hnwiMoodZndNs4Prvh+dSnSbfkTK3Y1TPbqRl/opoGUcLzPcBOEAACAASURBVF682G43OeftdltKyTkv8rC5vSrT9vmLF6enp7vddrU83O12AJIfZarjOC4OVldXV48ev7oaVufPz09Oj0uZdrvdanVATJvN9vjoRFVvrq+J6Oj4OOV8cXF+dHS0XK46SPbPfobG8iY+qrqw38PwfIJDShK4ajuYyfCje1hOGUDu1HeWd6/yIrqr1FX2fpCCY4WxBs/5xHSqICJl9eVuiwQphuauPNRBYdnZUBGrvTd4sQcRWIowc9+zDou7qBV9psQzQRRBTzeQFvn0bcUff9nwfezVDlWLlGnc7cZpWt/equrpycnR4eH19fXl1eXV1WUibLbbdC/ttttpmphpuVoy8c36dnFw8Pz582FYjIeHnz77ZFikzXq9Xq9PTwuIzs/Pp+1usVicv3hOTJvt+t69++cvng85L5crVGHFT0E+5nY/G16NmeqoXwLDpjdkLkXBXrasDLJUm1XbefqxVo1S1bjACQqfUfMIM1nOgxpqmd3YcQFXLZvNosrmSuZ82KCLWm11iEGtAMGfIKKqkohExA4Ra1KKoNhCRbboppFQA87OT4f5BBL3Ymy98FjV9uirnwZE3bgjmPU2F8vl4dGxEkRkGsdSyvXNzVgm4xCW0ReZVGW32xJhtVoNyyXd3h6sDol4s1lvNmuRsl7f7jbbMo67zUahZRq32/V6fV2mHQ95HHcXFy/Gcad2oDG3iCBIi3WHYuVzT/uDu3XaFSpVyz0qkpuEpEaoscuDmDnnBKAeM9hYaf98nmn1vl0aPVLVeYjo9u9I6M9mKJhZzUVad6Gw3ZJEdQ94D7YW3hpzsnbtIhEUEXufhE0p1fJDJ5FzohiaVAcQ9HEPfq2ve6PsiWaTa+Wjve/Nw/L43v3Dg0MFDcNi3NkxLteL1fLBgwdE/PTpq8+fvxh3OwUWyyUz85BTSothScyr1eHh4dHt7e3xyclmuxtyHoaFQsdxWh0cnp7e+/Djj4aUU8oPHz365OOPTYmbbiCUI/rotjPz7NonGHuPH0bfD1xRqx0pDDUMxgMxr8W2usy5PHWmpL0I52hB3O9GUFUF1+rToEdCfpgxzL+pqrJVRjGslqsE04sS8Xl4aIpuuSSI0iileFjjVDDCfstAMBS+nS0E4+EeRcQcRrc35E5jqkTbpnICZsX+LdtgM0LDYnV/sbI/LhYLQB88COACDg6PmfnR4ydSpEgZd2NKPCyWImWxPMg5vfr01YODg1cePFyv19vt9vTkdLVc3tzeXN9cLYbF4fHp63lY39wqYbE4ePjo1eurq5wzailwFZb3v1ojz31eDav3nHvVLcy/JOrAyNuGU1zzP1ltezlTlBnMAhANFJuHJS5DdAn1OAkFTc+cTxNEpmkySl9ErPQWvqJoPg/qu3UxjVLzWB1I+5QZMmVO01ioRQNUO2g4Zwd/aVRCdmGqx8boNDW6HV3vHgrUDfMEs4WOXfe2H7NoKVwNYKzBrsa5q0LEKaeEYbFYRtYjD4MC+uDBA1ACsFqtqms6Pjk9PrFFfj4ZhpOTE9OMnIeT4+O2TnWX2O59qJ+ZSCy/7BN44+gWI6YYl9NcqwL1gD/bixnqtLrE/bzeiI/r82antATx9fMxIxvkTlDJ37CRrDYNvvouIJbOd0Rkqb7+56G00n7kWIqqQkSUSEVSStQ2+5iuRzRHtFchGoOwPSQK2Am9tg/eBRqKZRrWua2YqEqlm6gBg/3iUrWyE3uA2ttuUJPpqtNU7NjgGaubIxwRaZepIGLEEWIW1ZA7GBAly8y48zGpW7ys3XyRBb+1zXb0hndAogbQ/H89fqsyS6s3IrhjUZCClZmSGWfdp0YqWhM39YTUOtTguXeKBwjw40Sc3qsdk0JsvD3n5OOLHXqxglgMD61Vo92mP7ZUYHmEii6qakdO2LxO466bDStS9kpa63IgNlWkipBN1V5KgUYhiVpqgIgIXQwYumMaRlEc5vbGta7ZZ8Aq0atq9FRPFTlntSMuETFqZQBeEbZ/V4eGPq6uUzOHQGyyahhDBDF+Gtg8c3+erLdjsimYZ511BhDHfKhUihrjIkIiyimRbXf0/nETVgXlzlBm2acYgh9xogpAiLTWCyn8jVLkK6LNfF1NKPkSBJEqQTnzQmZnc9l7c+Ty8lqKnN47YSgBTP5anTiXkvwtAd1u7Do38Adr96XrKPkyCLu5xoh95Sc2gTTDbepgGuN7Z+3pbutWtoSG5Npqu6mG0B3X75nv/JuqKYq6sPASRueCArymBd3aqMNBJRraKWj9wTEiwqj2ofrWAyUCmJhTIJkCakUaTLHCT3XjbCBKXNrRghlj8J/rMPsukJIdyGrHoauI7X7lxCKSbHNqYIOpzSR6cX7xo/d+cnN7mznlzCmlw+PDz3zmNRH56U/f/+STZ0+fPvnab37VjiUkosvLa5NZ17eaboh+djGU9Qy9sLRtpNHYLgDUFtTmrFbsd9McmhUOdB6RNLoEAjVFpP6C6kBjRls/e38Jd6oaRQqzOsWKsZ3BVFG0Xux9eseiAHxFpZKhHgu1LzbvIz6N7+2brMSCqRN3XXCpBRJqShd5+OpCfQrJN8H4LBpxICiB7KVU4g8uDBIr5FAW0bOz5ykNJ8enF+fXP/rh+3/yJ/+ckGBn0BKlnH7ja195/Oqjm+vN+nb77NPz/+uP/tk47V65f/rWm29sNiOB4ZaH9nTHlUqbg6i0wDXk4BuU+gx1DxJ1jNxFNKFsNd6F9BWD3nTYF6GdOmhVHORnFto39Wz1BpRdK3jZx7PzpO7H46q6Uu4T5u/KqWwnxAKQZ32ivA9Vb0J1eqswOmZezfHZTJ+hSsTk6ccGtfVOT+q0U10pSGy/JbIO3s5hVMXNzU1IwF6poZYJdOetAq9lIRG8996Pf/yjH7/55mcfPdLv/+v3np+dv/b0Mz//2QdG2VX13r3T52dXV1e3jx8/unfyyp+992fnFxcHh6tffPALnYqnuRWqHWPUCipA248QXrK3+DZZ2maw+uqZRVKMd+YcW4va4u3m9xzrPEKprKtCUYi6Q0yyK/drffrZcXbXeh1fV3j3UfelVQGbc1/WC2HvYfOfgoqJHwjdfD0BRPY2iJxAlVyZAtW4hCIODv10YbRxmpDUs7u73c6rUoIhKLS+XAAAVOyQtA8/fv7d7773i198eO/00WK4ff/9X6xvb99++6033njKTCJClK6urj/55GyzWT958jTn9OLF80ePHv7aV750c331yiv3flCmii5+HHPLJ5L3Ye4y4oe5gOpO/5bDnc10qELjVdCaPGrXzzz+bAVpv7U2SdQhRXNQL5lcmvdjj4ZX56i1EQKjnWxZd6LVPsSTg315im1uON65tlnWMZoAjRw3c8oMSO6bQwX/FtqYoCTO7Z8tAdY4WbRbTIiaJk96AoCoJiJLZQsznT0/P3t+sd2MpWCadLedbm/Xm+3NX/4r30gDRCSn5T/7v//lD390udtOu+1uu5l2280bb7z+7rtf3u02SeVPpFi1dN/zXuzVDzWdvzNBGhq3rzOzS5zjNryes41Z6EoODeZj9xq7S2I7DLRS0vmOLotV3JBrG61QKbps2FBpYkWmlq6xC8LQjDv7SwpaZ7read2upH58trdomBP6T2Rvq9JckTJsal/Y9oIimvW8E3LVLYplz4B7015CvZ3Yqx9UBSotYCFmBUopwzLnhYrQwLmUsUyTuZLFIuUhn7+4+OSjZze316shEYj9kOQYFDz43etlpXoxc20O4995+nEm3P2m4lG9YyLcEVr9U7DJ2sI+4w6Am+lP4Hxdtgi8CVidib8pf01uxDy3EdTVov4Ps47NW2yaEEA1I0zVTg3Cc9TZNsRr46+mRuTJoDkaua9yM1DEyxFrVEHtDYp2ka+xx7sgFBAmsL9fxtaMBbA34Vm4p6rllQevPHny+Kc//dk04fzy/DOvPk6UHB9no5svKwHUVTHdcRk+KTFJFdC1TlUz7k79OpLUTUAcuGlqaVy2zh8CB2bQNYe1eqpddELhWlSLG+NPdfn95e107fskxqoAgk7GnFFndb1GNnWiWVUJEXGsPlhQY+/trG9iC/XvmIGqx3VUJUl7gvCPvaylOogYp73YKJZEoFKKQq2AmgEyUCJbsjWFVxGx96l5zkmllPHRo/u/9fWvvfHma5dXL07vHT958thS2FTnVzVIWgk4dH/kLd/ZgzMP0rtIpf8K1QA0UiHdklPvzRrIVJ/T7JDmKl5biKe0G14q3joj7pvm18xamDe414jNY6+Ue/2vFlLRoupWDfh96yH7e0VSSgBlokSU4u2n88crdz6iE6i9MNH9QRtN3e5D7s6IyDcytFViIY/jaDZmQG2rDdlqv6VsoCpFdRwW/NnPPX3js6+bnSx5+M6/+GNvEGLrHgQC1V1pUilzFZneqS713GAbxoyshJvTvs6ra8HKEygWkaibm7AirYFqZ9AeUPZyBoKwaHCr9q/2F3bcPf6KcEFVFeb9vPvpNaZmSaBQL82NQfiemvl2/YpWRGQapYrsJ1xJpTXN1uqeLvf/4RVa3KaoCAm0uFdUElqxXJFCXhtEADHRMCwSM2xBV4SJIF5t4jUnCzW8kUYTC7FtoOHFcqFSIm1c+xzih681V0n1Ew+Y953xCDMszM6OFXtTO9qJJBrFgL3GRDaveq8K5xTLctRPs1/ar67Pkrddu+37mL+q442mdWot80oYzAffpyRc1Zh7TarRfJ0p7Xx9BVfxLG0HwITMVtLGCbTHexDZzpn+E2BrIFBFFPy0jlt3mZStDkmJOCcUUX8nLymRpmSNyx7wliL+wkrRlJhS5z/UXSrZG3DFM5wAYG8FrfvEq8r7JzbTgfrzZWLuIx2A/YHuDV4Ve7nKFj0B1BBI71xTzxjzKSQkF2UvN+tHJMGoQ7gO8UN74sm9Q9iDqWjEp39P1OE6qLu39tAEElwc6NdYmVJAicTraTV3OjWj1f1T9//UJfgVHUZXRlrxkomZxiL1FSLb7XaxTPcfnJ6cHp49h+okOipGkBwsDy8vbxcbBrBc6tHRyZCzUrFEKDHbocxQIBlovYSkBC2LCSBH+Bp+UzcNtH8jBfS1HFJf3daSGk5R5guoex/PMvfwRlWGTrq7mXWnOUtEvfzlGa1D1ZO1b16iMS/5xiZptkzbd8cBs1KlvqlOPgCQcwYowx1Hk9BLOtF1pRKaKiPLM3bn68LWS52YGWaQiigzL5YrEB0dL//iN3/r4YPThw8fvvb6o3c3Xzg+WR4dHf+P/8PvBz/Dq68+uX//dHWQD49XnH1PABEhJUo8qYBJ/I1gwWFDHICSlX/MjkkIrqMRVXUSofbuYu9AeAxhJvO0exw8mrTbZhVPVVb1udSt2yBQS3sHOdPEdvtdJdCu26H92vccHb/D/GI0h9sNp+lSLf5zYic9uHuVjVh5RAlypKq5M5cuITGrpmvOLgZWh1NJ6GycVRAgYmJgincH2imxkjk9fHD/4tVHq9XB0fHBF7/0+dPTk3/+T7999umZgplZVNa3t7/xG7/+a19+59XXHjJzLSxpzjdmIGjsbMm6CUgpqEmdn+h4HRTVP3RC9/eF1Lm8M0JVKIJh2MOkx+q9WdpzO/Gdz1Mv2fhL00ydq/BLGbTBWc+HdO9eIrTROWmrHsZv0IhxfQbruQCdLRoKElGcRtS/lq83TbuYQGgh3FyG7UtFrcWuJ84A9qItJiIGe56V7eWWPE763e/+2Q9/+KM333zzyZMnB0er119/9far7+TEu6mIqGhZLZa73fj+T3+eMu7dP0qJKq2bRWH9Op9DUaWH4eO0sUXnQOqHfdtLQjHbC9Hjsd2V2q9e32PXKxBY7Q+pqlq72iRGhO616REXqHr6NtJ5dS7cQPzibo1uPiMxNNWarwEIJFrmLzPtvWH0+07liauTbycnaKsw9Lo8qt0DEdeAf6b1dT4iOG31FHs24dhovbNJjdjWfrAUAyF8QrxpkIhvb9c/eO/HP/nJ+6cnr0yTDCLDMn/xS5+//8q9SYoUBXB5fv297/7w7Nnzo+PVW29/JudsUgwzIIBIiZs36QnoHhnV7r80I389Sv0yTO21oWFX/OklkdZL0aIJbP5d51q6K+6wkP2nxBxZcKCzTgKoMW59zvyxPu/SMgXe8izF44ylUsq9om7y9KNvMBLy0zNMcFSjUo1NWLN9FdFoTUla9b6RILQtAuRTzhwCVoUyaLcdN5udTKoCEQshJQ302mdeNfxKxN/73g9v1+vtbiftGq2976OkcEgzO94XoO8onXHq+CF0UhXtdI4myrverKc0s0+lSnddIAB66feuxRU+AbxE4Xp639/apxNtWjSqartcQD8WarCNKpKwourvKHDbO16BWEwjPOh2m8zNJ5uytEeqoG5sVrSlqwrGGlbZlW6F0lnvFQoGqa2lCaAM0mKFOCwC254saokvJYDUXklBZmfG00VURfwwwCqbCuDdyr355AYUs0jKd2ftMRDEl1p/ba3VQpx9pamhXPyqIZaqdi/TpLk/om4LXvzc2rE50jtMiFpNRjeEioR1jN5D0S4nQHsdsLwMatjoMvCBh7L5210AgKPc2rMvXhhtJGZvxBRBM6rPjrFZT8MhuASYa70FEB1SFFsdqypeKXBitmNco8DXS30FolpEJl9UB+yLel0UZrbJ6syws7/mc6oT9O8b+Q5RxF/qPqzm+HvhvJTV1vbVFX7/j7PfXDh3j33puvSyZ/UMN9p9eWfaHe2uyteqgmrFvL7ZRr1f0vWqDWDnKo5qBmzZhLXHtLTtDW2MS2N1yg+kNgUmgKz2vllax8IsOKxHb8NKdZmTR13OVGxJQVSVmUW0QDh5mlvsbXAKiFKCSrdBas4a47+B9c09teOtVL0PcJx1ywiySRTF7ZVrU8vfaIi7l43uAUacYqMBJ2IRUnWae5hUZY75W35qwF9/7r/fu2BvzveKHfpAMK7v7lKFn54QLk/V3lqfKIshHxElVhCp2ItNBIiXT8xeoD7rRf9Jycrv66aOatH9v46B9kIWW90wucTuZAUwTZMUKlL8AD+oiJSpCEmXRdc6YWp7EgyqkCL6g+GcdjSi8y+dLjc6TGga0FhgP48A4G/DwlwtyGP7+TaPO6HJvrnHZTRr51d++lFU8K7hc5hxOGmzEzTxVuDtHsRxGsnLFbdx+SoX3xpKXooZeVFmVtHwleYaOHEGkImgWupBUq2oQJubKCJxkgx7v92X2LRIp+b+qkNfd/AUtGcEFJoSqyDnwZ1aEbTlPK10pKjCV9wsXeRr/iKFWYGhTXNnYXvxproVxSvblURABHb9k+qP2oe0FaP3E9+5wrqG43NzB1mcTnXhT+SSSIkVXV/nYNPrbq9w/pah/UsopolEJeix58G1ixUoNv2ge+6eO1OPixyPNPy/zgQMJqobCOGv+2VVyhpp0B6K9x7ERLAjW2taoUW91QUrjCU5NCmIU8opJSqSUpqmCb6qR5yUkzIroEVKkUKJSHJKqexEhDgRIdnjpM6ZWpEx1XNqKzBqZaTdIacur6jasfmiqOedU4FwX+2uJnH055N0cnfRe0WOR4hVO7rJrksc4UG1Lp013jPDqjt+yrnqnHpEC3EB4B3w5FYdnaFIlxbdozDR/KzddqEdDA/zA4ZMIkp+mhtQNxjNcbjR4bnfpUr0m3GCvDRIrdDMdvUzlMwZ2UUiCqYy+VPyQF95953Ly3OlKWViYi24udr84z/6Ex2nlAZmPjm5t1qsVgcrsFrSXaDJXyyo4zi6LnvH7ESkEkyrhf+h89RxbeNqYYKNX0WtYS/lWJWr06lEsXPMLpM6L1Z5og2htN5kCsM9MLyMD8XsEYFRj+3U7r/9NCAiD1eD7pmdFgC6v1EgCBpaqOLbjG3y/XVqmN1SCZZ66hUIv1TfU9a8b/eg6ExnjjNFrsQDBLBqK7dQ1XqMC6jtM4E7OHr7859LzMxpsRwAPX9x8c//ybd/+P0f+sYYpWHIb33+7eVhPjk94sQihRIpEsHL3CytrTVzMjdg55hdAWHHedG9/mf2hvQ+J96Pt2J2DDtCjLbbq/Nt1XvsT11lNVC0rV7hN60MWc1RVhV/ub8TRT130O+Op72EgDVL2DMJ9JvdyDHanU4zUZ/hINSxIyCCJNiuWeMHraOK2P/VNNJomt6hmfZxe6q/12H43kffI8LMcUhDGRbpjc++RkTDIqmqlOn84vl6e1vs1coKIry4OPrqr3/l7S+8OQz2lghnfCKySINbUydiqscooXqcWT/jBObAFCi0lY4gMrp1x7wNDndsKFyM/cN9kYl1qlKrKoj6n/rZY9zU/Q/oliBaSmLmd/wRLXSIMfjQGDXyQi03nvvENgR0jSFWUU2ZNDZds3GWnsQQ+RJNe2uiSOipKVk0w0ziqY67OX7X4jnPMDIuKmJkVCYthZx3ISdSgASyWq5Ei8hISicnh9/4C1//6q+/KwWlTAoQ4eho9drrr967f0KsFShUVUQWi2GvMxVGq1lZcbf6eJ0varPZftuNDbx7eWJYt2GEuoyp7Z+YgUQL1fYxsZ85rf/Z/zSiTaTSv1lr1lb7uUFRYxndNpIaA8UTm15UQOrW8Hqa78OEmmuTdktfxBHd8G/6eiMEqHVEDd69/jimKlwnzGHyGu92tN9URIq8ODv/1ne+f7OZVqvVer1mK3kjEFNiuv/g9NHjB8MiL1bD21/4bOIMUCmFQGBNTKKTotiDevZmp5RExN8PbTa74axxd4qratThm/S7pHWvMfNaxDDi3kd0MV2nml2v7nzpdt/BTV3hovldfRf3AoFGieKe+b8gC1KonmIRNjIXwF5YuqfE7ji1lkgCDGI7U4+QbU3ei6o8Up0pnbHA7tc5NwpxAN1+OXuwAoqzZy/+xT/7zsm9RwQ9Ozsbd1POeSrT8cnxOG4/+9Ybp6f38iIXLSI6lZE5IRyMbaurkbnlLShxCwIcCPc2rRrqIA570Ngirb245geDmgeMapaZN29+bA9PqslQW+aX2oFuevZE1/JNHaOZz5xWMOm9ngflNlM1i2At12trTTbCfc+thWJOX/7xbE6VsFYJtzF5WAw2kgptFBsB/n3PNYyLiDyQbmFdY2d2yBYIdWe4vZpUiUiFEg/vfP6dlPDT5SJRUtDZ2dmjRw8/+eTj9c1mmooq/IQ0qoxOCLVYTIMdk0LsbTPO7irq9OvfsKfzfNimPlXQ2v2pLmyFBGIOA9vqeT/9xajSqFOEOAJpz5O9nAp4X3TvBgAVpGaD8rxjK0gJiqd71o2ZN3XW3ANn5+TmaDpvxcvkw1v2LMIoIadAoyqLLmik1g03clsAmaVkNGZDobFDyE4pVfc1qgRKTNNu9+zZp7/9jd+8Xl+89dm3zs7OtrvbnLt6KOqGqlJKqW9RmQufVFlE1NdxI88GqNoZqfax85KtErwihdMju0PtMuDugfG13kWDS4X+hbOPyYnIi4JzqlNZighLW5TK2r+LgypBaduOAnzCn7bl4pg4gr84E1SdrGocZ9iMpr6QgyjeFxeKUg2vF617Og9KSDzkrqBioAR2Uusn1liFtN2ee6ofjcZpw0G4jecgprqKT0JJieCbvYtAChSqul2vNzfXKBNREdn9q3/1rZT55uby4uLF1fXlweFBygSUUiYR8VyOaNHix9Sn5IplQ2JfbCf28yQrKphYQvWbD+r8XUPQDtWrfLu5ClyrZSUhGacUaF4DnW+d0aAomQtgmiODtn6/BEU6TeradK1uacJGcOZzV3+tblUtwT3DErer/pbw1uGd5+ge7RI87iaC1gpFIsqVH1fV8ygg2qmVjQRn5j70EB/Fu5FzzlMZVZWIWfHJhx/++M++94V3vvT6648+/uj93W63K9N2s1seHqyvb2+ur6RM9145pgTRgiIAJaIiEwCrMSICMxEl13UFVMs05jTEjNV3zav/24TsMdp8nmj+M0WS3ETZ1SSRT1uvI92CdXsEBYwDoCgDiwPkneH1ajef+Lnjsul0ehEEhXyBIxJggV6tlUZbHDvF0U0BSuhWoIMpkuVh5st2FG9dUg1BUk37s9qpbASVbKfSUEo1i72nlb3Ko9ZSVTF1jLBnULbD9ebm5uLFizKNKS9IRXfTP/3Df/TRx5/8ub/wl97+3K/nvDg8PD48Okopea0tEwgFZSqjQFGUmMbdLucM6yRAk4KLHdkuJUp/FzSOo6oQNadA6INzjYBhJnCKoAuBIvEHqhm3+eIJUVOERoka8EUD4u6G0OWVazikoPkhD66J1F055y4NVJv/CnAL/uUrKrWpxlyptkEVmO2WyooigAN39K6qeeBcPDi4adPOWL6wQeWQ8hxm0N6eFqdgCdq5gmSjt0UO88VQmqYpclUKFYJuN+sffufbP/nunxIlELcKk8yUEuVhcbBarlZ5yKvD1eHqEEQnp6fDcrFYHiyWy9VicXhweHi4WiwXOWfbAIlSgPzphz8XmTD7OCkLMI8pn1+BcDOB/bUyM3AMtTwbaBV5OkMUtyqBhzLV8xE4UUyg1isdGEl7wbq9hhkQoYXE2tMkbqdJ+oPsshK+ucPLauW1dFAqj1ObzXZylWpd7FPx4wyaqjg3NEWOEAdWme6KFcs4lRuh4dleALL/6XYVhqbbaZ4pJQKRCqSolpxSjkL80D5VswEVKkVl3E3b8eaKmK7c+JlSKqqcsr2QJXnvlZlzSgRM00625eTRa8FU2rTGj2G7DfQdZirXRGBMhaQwVnhooGZa7RzwO/Kofs2kQbjzdwI1ACDAdrPXYuOYM+02A7RjOtXpJuIVoVXbKZhgW8aqHarjvVNfH3PWWE+3JDlPO/jMSsywOEPtLyNi5jjSA7l6VXS63Gd4u8QJz0HXXLi/QC2lNOTBrlORMk3KEg6AfBe8RShMOkXZihV0E/vyptEcSz76nMa4LTAUUYgUnL7yqp1Da+/x60ZY/5kxAZNPawAAIABJREFUUA9FI/3WzcI+y606NVOtjnyg2myVRXcqvE+jKoMSM1NSle12vdlsNpvNuBtLGaexbHfbabLYQpnzkPNiscjDkIecc14ul8vlcrVccfI3+BSyl+9o83GBXtUM4tvZqDuhaAiJAyirr6QufIBLhlpiwXGKOFXTVWWi+oauzO0s83rWbIeoQekrO2wdji/MTR4dHf253/r6T37w3Y8+uJVpEpko1Yw996m+xsAqDBii2SrrbAt4Bx+oxVkqwvbi0mhj3qFwJnvwEb4pskcdN9pD3zmwtLQwujnbI4sqauZrNgrCi7MXP/3Rj95///1nzz7Z7XbOSDkvFovFYpFzzjkRx3vQVEuRItM4FZEiIlBlSicnJ4+fPHn9zTdeffp0sVwVW3EMd1WjHBuO9DWrd9Gx42PBD50rNmFRxBVu1BRHBdtiCNqjo2rEZJGrYc3Dhyr3zvM3yjbz3FAQ83K5fPfdd/mv/6effvTz3XYzFfExA6UUKWWapiKlTP7DVEopU5nKKEVKEVEtMk3TJNM0TXaHSJlE7HQblVKkaBFVQaEhp1lASVUVOm3wWrv4Be6x6tzvU4rOQc4UK6oD2pVAzSETwEwDp6urm2fPPv3000+fnZ1dXl5uN1uRAtXFavnw0aPT09Pj46PD1dFytRoWi2ExJGaKY55EpUwylXG3HXe7ze16fXt9fXN9s9vtfvHhLz765KOch9XBwenJ6YMHr9y7f//Bg0dEPMVudOsTV/7T5QI6vjtTrH45mSoRavyvEoKoIGOq68XV+FwOoAyqDKzXW78sYL/KeI7/7rbVHNvB6uAv/MXfkWlbxJXEUNFVRkqZioqM41hUplLKVETKVNz+tFgJm9h2kWkaS5nGaZzGaZrsh52UItO0G+Xwlac5Dz2c73GXsAE/o51q1NFfOYesCsQaeZMam+0P3P9rkaE8+/jsT//0O59+8slmfcuclsvlYrl4+PrTh48e3rt3//Tk5Oj4eLU6WC6XOQ/MDN8moOr8wxSAiFQnLWWcxmm3224269vb25ubm8vLy8uLy/V6ffbsk08//lBBBweHrz597c3PvXl4eIi61NXzwZd+yOE77K1bjdFOGk57K847A55vH/cT0O2y/Ese1yCqUSMFfEO+/e7KV0iZoWq79PPRySknbvy7k72q8v6rKjqCi9otn0JVKaXYu8bLNBaxt6uXzVg++vQq56GVB7Ve1tt99sMla+3NjE7e+Wi1sc6aA7O9UJuJyjSevXj+8UcfvXjx4tknn/74Jz9aLhavvfba22+//fZbbz15+vTg8EgBQ9yiUopudqNut4FjPT2IeSJnJwwaFovlwcHDx49SyonTNE3X19cfffThh7/48MOPPnz//ffPnj9//uLZvdPTk+OT+688ODm9xylPtlW0jkVjxqqFuCxi483LZdDMhsL5E8V2kEqWA33UCkXi17o1who3Bh1mAsa8lpQsGiDmunUy5+ViMQzcVYgCBA8lFSCq1L1FMbFo7ABQWmqfiTnxkAYsUcPUAi1CaXltR6L0iNJ7oqq+3DpSe07zS/zGMLw2uzVo7QiRSinn5+fv//SnP/rxDz/44INSyuMnj//qX/2rb3/+848ePVouFlMpu910dXNbpDqdoCLuZS0wqBuZXY2aJtdRMGzzccp5dXj0zpe+/KUvv3u7vjn75OznP//pj37wgw9+9tPFsHj08PHrb7zx+MnTg+Mj5jRL7zsQR4MEtfMXtaUfA3JoltSIztif64tLtCKEn0oDai97mFf9UTc/7K9RUg+7bE3Bz79xduAn/uc8DMOQ7dTG6pS175eXd4TP8Cm0RmIWe4E2B27xha3A+OYTAwZEmVWIKRb7KNjjPi/ormyyCqHPr9K62EOM7WZ7fn7+/Pnz73z7W9/69rcODg++/ltf/93f/Xdef/ONcbfb7nab3e52u/GlxZn92wOJCPHS9zphMyZaf/YYWwESgUylbLYj2XkqKT19/bXPf+Gt3/nmN3/6/k++/6+/9/3vff97f/b9L3zhnS9+6dcODg6P750sFssSfLg7wkcrNszGux+r1iMPomCouS/UxojaMTK5up1Kt8zywjdgXuBl2KZx+q6vHqTYwGIHAyard+zKY/pkfF2h1BmLj8WmHOj+stk3zbRXSVUIDXVxWyFnAEpExauzTUW1pmc6GVJz1N1hZR3mKxkEjdN7f/a9P/iDP3jvvffeeuutv/E3/sbXfvNrxyenu910/uJCRLRfSPeW419/MRPsqLDQbe9EP4mOEMQeIiGMxE/YEVWdpmncTre3t8Mif+azn/vsW5//5l/6yz/4wQ++/S+/9Q9+/3969PDhN/78n3/r85/3o3nIXx/VkhRe2NzJlilWuPdxiAzblJSSkkT+xoYJjnPMcp3Fytwr2QpVnU1mz2ZrYGkPApDAiZjjpUvSUyMKB90nMzyKt/fIuKkEcNY6wNnTM7ATEFPyIKL3YmZAzUcEFbPpAHXkqU1htSPqwQO1WQKuri///t//+3/8x3/8hS984W//7b/99W/8dhFZr9eXV1e21de8RR/xtdUVp+kuKGrmHpyhq2mpaZtWaqiIN2rZBBIgRYsqj9NmvdnmlJaL1W99/Rv/1m/+5g/ee++P/vAP/97f++9//de/9u/++//e6ek9OwLKh1mrkrS1XQc/J1X+miW7zWshWqxKIDBzSsl6PqPYVcCV93a2y06I0Db1eK6L7C3avkBrf7IIxDuicT3MCc0gNs5hJCKvQNBwnhWl6jQTIKqZE1ntETwfh9mxjK7DMXKSRnfssQwrjYo9frakoTXgqSSU6Ohg+Xf/u7/7+//g97/61a/+nb/zd77y1XeJ+fr61nIUdqUnBGx4NuvodZU08oR10mIPKNxnANoy5tZsT4Et811rK7L6ZjlipXEqU7ndbDdDHt5+50vvfPHLH338i//tf/mf/9v/5r9+550v/Yd/7a8d3TudiqpQLbOD12t3eoPZxxDd5ULMQCL1IkpfnoOCOV6BkittqivdUIiKvwQR/n4XabNbh0Z1cutLZF0ZK+5UIfkf2gaR+Fa5k1el8TOv0ycrVBPRpBg4zhwKS6vEOVDvLh3yG+psiqqtVSvaPg0EqRKRn/zoB7/3e7/3/Pnzv/W3/tbv/u7v5sViKlPZjVIiOmiusdqeFYPNOnDXN6sPt/51RowArqbfw/HdhLuF7moVy0V24zjk/ODRk//8b/7N/+c73/6jP/zHv/d7v/fN3/md3/ja1w6PjouAQELhAjS2l0RTd7DfOUDEYQ1Qu+ANaK8s3ivXn/3szD4wTqXxTjgDIPK6NsAQR1u3SJRUJbm054KgSKdWD9lDTxf9h6rZFNtZcfXCymFt0M6gEF787kxq6wsDlX2EKEDn5y/+6B/9n//wH/7Dd99997/4r/7LV+7fp5TH0TEobLlq+2wbmLMxms267boihqrcrZWrZ+Bqc7vaXWb2WLPIKIGj1HIHUGiRopMUnRY5v/Plr7z++ue+/e1vfe97//rHP/7h13/7G1/80pdLnAODPTi6E1z0wpL6YjZ3jkpQJq2ON6of+3bcOmz6wqt2c2wuqH9Q01aCRmjQd6zPBuEOr6zxcARaQM0aA517tL9bHZ3WLLL/E9yqeuF5lNs97f/t7Ft/JTmu+86p7nnd1+7e3SW5S1LSUitKFCnqYSu2Yid29IBkC3GMAAECxMhXA/43guSjgyBA4P8hHwzEhg0hghFLsS2ZJByTEimS4kN8L7m73Lv33rkz09118uE8q3p2FWRErOb2dFdXnefvnDpVpaRIKanRhKSikIf8ztvv/PAHf/3yyy994xvf+Ma3vnnh8HAYqOv6kI107Gjm2aJK8RyFR7YuFkeEm357ZovFRWu6o5QAm0+bvJL/s/aFbnyE3TBQ2zZ7B+e/+mu//sADD7zwwv955pm/v3nz1pNPfWHvYB8w5TKCqYCR85T4JDBITdKTpdkAgiV6IERqsUV/Awr6KaQmpCQV9jjc0X8r2eb6MoREyHWMUt5VWmnmhFO/EjcnHCImOTkU1DBYFZBUXhPGMl9J9XphWzkiNZVEP3vxxe9//392m+73f/9ff/aJxw8vXlxvNrKGExHFrNS9K6Zmt300HMSxiwviqDIu8mh3+iMZMqKec5HjjnoA1gJAzrnrKA80nUyuffqx/fP7P3vpZ2+9+Yujo6MvfflLV68+0mf15kBSZOduNEh/AuCyaz3fU30MQpKyNSJq72nKNOp2aGNBWeCH4bDy2Ug/XThLRFgem1M7ArDf3BNXvSKxSCklykP1k/s2txDepr8p/BJN3fJ0+fw//uNzzz07nU6+/vV/8eWvfJUgr9YbkmoBBJ3RNEoAQFh/cj/PAIIcQl1X6fXGGClc0YREYuWw9YiBdNpJQymUM2Ra59xOmgcfenh3Z29/b+/VV1595sc/fuLzJ49/9nNdHmpHBBop+xXmN5pMB3lITSORfpE3clhi/lhbEuZUvsEJ4msn4svUqdhXwTiIaIFVaE3fK7+4MQ8sd07KCauus4VBVS4RIthaoope8lAGAFqtVj/60Y+eeeaZS5cufuOb33j88c/2w9APcijPFnjleK4GfNrlKKIAYS+lOKCCAKWJtI5a8yYx0e/L42gARDIm/Joh59xBJtjbP3jq6S/u7e//5Pnn/+Efnjs9PX3qC1+AphW7Z4VTJe+CoOvLbSaYA34AAGjBpr/Uj6kx1YSntkNiClz/mI9J13DGM5edGEFxAQl4Y5BId420dSQEYBUuIgwRlITwqH6dAnYRw0wkaYqaHQr1QLJMx8cnzz777I9//OPLDz7w7e985/r166vVKhz0XMi0jSt0qqryqwSF70sJkU9pDjA0PsjAHVFFwQlo/CAlCmYAsileBO8KKcxKvBEiABBx0cR0Mrn++OP7+3vP/PjHf/+jH02a5jOfeyK1U0+tSWvhrUiQ4wbD3CACz8cnSbV5oYjEqahcCua7iEu8UsrtDyLXbXk4HymKan4SoGXADI4EUtkiJ0XboNGaN8sOHVODtCEQKBVtjNNDlj3J0KxfusIEpFxmvdk88+wz3//+9z//5JPf/s53rly5slqvQp6dhbqc3eb8oXCXEE2u5bK4zYBb9E9DzaI2kQw28Jydr+piWIhckwSPZnJiGQXkaV2iImYqb7q+bZsrVx/5rd/e+dHf/t0PfvhDTOnapz8znU8pMERzyepLFdN5LAPE89OyNhV5IyFBPLKjCumGKKrwKGlWjerl4HOyeSNC4hPEKZo1AiKwU0H4LVLdiH7Mr/BAyx6NGCSGQp2g7rOW9EpYyItSRszvRFFye68zk+NIXj4HkAFpGLoXX/rJn/6PP/38Fz7/e//q964+/PB6s8mcPpW9H7kfObxNzaJErNn0Bys2ABHAINkU0lxRYr7wsioQjULEBj3CzzEaFnss0QSZuUXKyROTSWlCwIfSKTEzE54o56Hruk3fnzs8/Po3v/m5Jz7353/+Z2+89sp6dQbR9I728uLKev4vITS8Zs1O8AhFI6pM/nEHwVZBghQVCWNwkn2iKEVTED+UAPSVLhnSS/dZAYAQgNoMQJMAa48oYePxZPRkRgv9gv6ouUOPEN97773/9id/8pWvfOW73/3u+fPnV6uVB9/8jNAG1TAnMsRA/i5SzBCckYQJbNrVnPsgo5RYEk4RblKLZT0nU/YRdDCZdT+ox51pL8jugb4fVutNO5t++9vf+epXf+V73/vLt954s99soj0bhwpcbq9WiYeFqRGtTi4YNkbEhsvyXO8oqLxxnZUpD3I0jObBKfYnhXlhLtuWpwB41VtCzZdbHzCoNWkmHAAQMkqQ2yA2KmNGR8zE80fMF3ZeGXULAKeCrgZbL5f/4T/9xyeffPIP/uDfX778YN/LIaxiL3n2kUB0c4x+zC5iykCEWWIxTLw5n6yeAF1GF95urq0WBwIEYRi5JhOE+zXeBRkU6mygOB7bHtPFnBMgcng9UR5oveqoSb/73d97+otf/su/+IvXXnk5931QQyLMNjFBfFgD77CmVjqllJJB7IAt3CwJ6BUv6NEc2oBIeQ1aCmqxD+qMKUnjYmULMwnAK+3B1E6cR5DokM5xSieu/xdzVXGBKRZNxZhTiIQ55+XJ8R//8R+fO3fuD//wD9vJZC3qiOE+MChOscy5alFyXWCULMbops0kXjWzbETZwIeMZaUuFsLGO/SQYjYFTfqvg6FgxmoTTfrGzaqjyeR3fve7k6b9q7/6qyHDU09/SacInf7h5WrXRcrdaLXWsG91hegTdwQy22bF5zyyGGUjHyiTBEUSEUFKmHQzSaEbimAFsFtQSIq1ZDoPmYG8P7/JdyYRs0ZDB14HrA5W8zcyWOQJRFCZU8gIq+Xye9/73o0bN/7zf/mv8/ksD3Y0gvQuqcu1nDxYm+pNbYaA+xGZZpklZTFEVwUKc5CcApr13VqNL9ZLu0R6IgwIucUgyc7J5nqU1MlyezpIAmwGytR3s2n7td/4jfVm8/w//kPT4BNPPpV9cZUwGLHh9K1D+XL7NonkMESe5H7LU4zoeMUV3abrLShx4SAusgP1JyjoAbGScdDFl2xDdLZP9Swlb9BbI0A51TsgELVqZQxocMI2jj09Pnnu2ef+5m/+5o/+6I92dnbqPAX3I0QGoLU4Gq5v8UYkkKqAKVWaKiYclJn+Dq0oisBMH6yMXIiK3JmIEXEMNKpEjHor+6LknDddP9/Z+ye/9uuXLl9++eWX33z99UlqIATdEe0ZA0BZxm/nmfmwP5K+iILegFgiqXhUsc5kp5AmhJTIeopAGim4xwRMkNR4IbI3BPHWpA2pzlUEUN4QSayNoQgVGEXofiYAhhLNtJtU5ZzfeeftH/7vH/zmb/zzp57+oi5fQQI+x4Yxoxfy8rkU5rS4ewJiCBNZwJjHW08rA/iHDLJRlVcm6UAj0qptEUcatqBaQTNBOfEOgLJTvvxRiCMW/RH2AVAmHIbcDf2FSxeffvrpSdu+9OKLH7z/np0gw5zMkHlqSamsL9HmErELk3DDUBlqVlIAIf+OaiQgPGHKqQfERNaH8CEKaaAelA+wjQSTv+CB0dASlnNY3BNJoCAFPTX7aAD7gw9uPPfcc/v7e9/41reapuUbSWliX6znpGZ4DIo1EtIV5SFSM/eqjI8J2wq2OcRzPHPvD0rsw7FYIVukdlTMf85b6Fu1BpAJhpxzzlcevvqZxx/fbNavvPIzm2gSPTDyBcFFnc8moqTpGQTxFcCAPiRyUNM84RgkjLIISKjQqiDxvWhBgMRlWmKPRTITgSqUihhGshbRozsuFXOLW/SKrc+Xlk6PT1588aevvfHGb/321y9eutj3PSBitaWh2tBx/2MkHMwAAEKC5HoqDjeqPhWmPnQpUDMjBK2oXJu/nQhz2RqF/3JQB7TrnnDCyCIFWATDkFPbXnvssWvXrt25ffvln72ICkMBQPNSmrtDBKJus1mtVqpJwtCqS4TqXfTsFVMVTdoSgASlwUPrmKsgKnCi9jNQxjlBq7NLkjpTmRQWaYcgVcH2FAQO9irnd95+643XX//UtWtPPf2Fvu/NFKn9otCdgoRQ3ONWxxNIge9qGB0AaNoimTqHfsoQODrG2IwbPO4k/8fcla0slJ6kVwxJUrhOBUnFNiR06lImGoa8s7f36evXL1669Nprr926eVOWTjACIbMy3FLebDZ37txRaxQ67a1yj4UxlpBzjEfuvcSjVD7hPqZImlCjAX52gPE/+y2Kn4tnJbkcnjApt9S6uH//3L1799VXXwGgb33rW5PJRJ5Sqah3jilbNnEACpogkiRECJ1EKRWVx0MIwA+4NOkvIleBxK5V9UeknzSWBEPftQcwi2kz4iQVAjIcDAWAmaAf8v75c5++fn1vf/+1117tu47bSYjdZjP0soVL5nBjGO7evSs3AHANm6I8BFtcAXLEJ8/esqDYgS8I4g55BBnldlcLI9Y2KhjkyAZlVEuCsAZopfxBhx3yoLlEBJAFR4BASOTnxlHOwyuvvPzuu+9+8lOffPTRR4c+yyFXjGADOA0dLcrcETmDhwHaGr1SHKnOnlqDCH6QRgADhODpWaW0QVsBEhFvRUrKfFwIQhOYUDrxk1puVpLkgsVsYFuie+9lIsTm8gMPXnvs2snx8e2bN7mtYeg//vj2erPhe/phyEQ559V6PfQDxgoKdaXqKZJ3WqY2hVt6vx8nRQDQtm3S5JhjSYJSDAABmjBdzBgka75TX8LqSZE34O9HkDSWKTVCEC0AYpwR+Xr36M5Pf/oTTPjlL3+l6zqI1jVOGmgn0SdfQ0CEtcNjsG/2Uty9zS0Vd9bq5OLkegRm7UjpaJg9NoUiOhisnb4dlEyF+TMiRqTpN4jrJBpybqfThx9+5OrVR177+WtnZ2cw5FsffXjr1q2h7xHlDA/i7Ra77vTkBAASYgMadSsBZeESD2YIgxSsLcZdLOUAyPhLHAphNv74fIX2OwyJEDPPAroEAGFSTGQiiUBJVq6yGUcAgAxZcC15UGVAlWNTFB2kN15/fb1ef/ZzTzz6iU8CgOzyCRSTKySzNCQ+EUkTeoOwVG10lAaCoZqBBtD4oVQDNPCCMjzAbD6diBO/NaZELUWIhplEsNzsM+kBqDSrKGlxsIlbyfHk0uWr1HIBbm6ns09+6lPT2eyln75w6+aHt2/fns9mTdtQ1nIg9SPHx8cRG8XclUp0iT7sWQjKZiNLvKwsQMjYEqJkOqMgIUEiQt/2mh0lVcpk71dh4We8z3msYQSWzSOi9Wr9wvPPP/LoJ37jN//ZerPmhWVkplJzBDUHSeVeIQiPo/CzagNLeKJiEjXQrJniypG/lqSYvpzsDaY7GOkhPwi1zfxn5gKgxwyWGgjhp5kfY5e2w26u2dndn8/nL/3sZ88///x8d4clVc8SwpwzAgx5+ODDG4ioJxiB1oqrWutXWSojG8GqtrHR4puSzB+PjuER9CM3S/Zf/H2szbUNlki9OIAmIVEGTwC6+ZgmS5smISAfRWOvJkLCJO0Tn51GP3nhhdOT009fP2gnbSZw5UGFX762hJvK5uoIBiLSDTe55iIU1St7mPzq5XXuNBAilKYi0MADdzvkWsnEScFsEwCllLICfEXEtiIRwMMx0FkL81i29jpYXp8w5/9PzimuiMtDatJj1x9//8ZHi52dtplevHJuMp1xI5SJV3gmwPV63XVd6w2zPJkCmbcHcWJOL1EdVxxXQf3YFHMJPCE0ZCJJgEBZappqSRSdUjMHyHg1adKPVNcZRZLUfKO4e8rrzer5F56/fPnyY489Jn1Lts8/qHX0ZZNY1OhV2FYtgilUYEtYVGoy7Y+T/o0W/4ofKxyf3RglTK6Qka1oHzSmKSEQef9H8R6vAuXXywAQuZ4HgHgWNQMtdneuX7/+8Z2PF7s709kspRYRN7JJnPAu5yxiRL4OVh1bnMolVTbtPwLPhkrCiTCB75wpxK5GZZRXp6O2nuvTfThCWd4eNSyOEp4TAa/IRsYw2dbkG8RCwoaIuIwOE9y++dGtW7c+/5tPPPLIo1xVyLlBLebVqFvjdpASPBTMr7iaJSBz5gr1pQCa5HSu6+Fu4EYXveJHeeZRt13VeAqKOxmMKuXt/vEpbLzZK42ERmdGqRBKzBWQIl68ExQ8pXTl4Yf7TCmlbuh3pjMExL7XjkkWu+/7uNeRvxYQtVAEmBgKMQ30qK/mKInnfolsBTFvOwfoc9iV9m67CrXscS2BAiwIis74WPGBMVwjKRTDhgCvvPLKtWvXHnn00SQbHqALADLGJcuD87NauePIrvAIJbHGBFSjE478IZNHDBi0gEeBTpV9kpdsI58RQ8ykWbt4v3jKMtxRqzlWdn+aiHZ2ds+fP08IqLNP1udG0tmw2WzUuxozRBhREb4VTzmpLMNnMy3c/SS+uoQ++h95nsJC6WoUyUbg5jpJ9t3eLrveiHO1vlk1iMXnhECbs9Vbb/7i0U88ev7wsB9IQYvYITJb5l1Vu6w5CFb6MM9hgoooE8kkqF/3VAfJDRJ5wYI1Ki1RKUNE9pL4XxACE1jUWVA3HTEVyWymHFt3r4eBIWC+VVkWfAVlnnpHhP39/bZpIdN6tQaitm0TH6LO+xsRrZZntoIeYqeVnQZW2LS6I6eqn1oOpwzRj+Qya5sD5irKF+uOtfE3K5+J6iqLA0KUIRyFwk3ABx98cPfoaG/vYDZfuA2SrtXfWc6ZY5XiakBGoGUOgRnc/dqZKH986trBnUAWgNhELUNU/RCGxsP0kTKfommTR9xFZTLXXRszMjmLV5iWOQ/z+Xw6meRhaFLClNqmadtGdyBKiLhcLo1t3kZlUVR0xG1pr8MjsjzDoKjO6m7pceiplGmNRMl+1h9JyWbOQ2kZ7nF7oW9FoJxfe+213d3dvd3dhA0pA8wEuTSg2QjRGMtVa0jsozVhdQtPNp5RnsIRFobnRWnQJTvSpiCd9Uf+RInkMeZ9KpF2J0fGXPmzfED9TRDBQkOQMiHCdDpFxOl0AkSINGlbK6JtUjpbLi2LL8ZcbCaVsu2uRvJGWpRhegapTMoBoBE32K1aqqwVCR24hra4HwvdREbIBHbgMEBRAxMmFPq+f+3nr16/fv38ufMibMiBcQbfI4DLrcDyVSAdqEBDBBM+IqcAgZXeWWSWoFRQAHPciECUzWqEt0AlRlCumdHN2nKgkj6kf8rWyiHA5CIntaNmpDBzua5jQZ+L4uFSzpDzYrFIKXV9l2kAgCalJrUJG146sF6tND8hJx0jkdEU3BB4HaNDEKUsxy/OgyB97kQCJ/xTwqBwHeqPwA9/DiVtIStE0QwnqJ0AorOz5YcffXT9M9fPnTuIJKfYbD1x4a5vLCtKBFY7DN/5fo83RyMIDiiU0tYIG1GOsa+pFIwT1vIDktNUYSjfqLFRfCJsLSlzkICIBAnFQAAMs+rVAAAgAElEQVQBoNQhAhHxRt6bzUbbhLZpTRiGoW8BElBCSOrDSc/nIj1IhLsqqxwtO+BZENTC6TAAqw8QwFEQxUdYENroEA+BBQAAO14NvXzZ3G4iraeNrSWEjz++feHwwv7BAU4a6gZ3mDpQIAKsSmhZJ1NQFS7mDkjGUqg+QUM6EWujo7FPp7CQCNyhKR146SARciyOlCxg5kcIATlvKXkR7zMggGB83v8sB+1iPrFQZHXe2oNEIMfAuSkWqhZVuPP5fBiGnHPTtCmFVVZIoHudRY6iLpzzWduAABw9R8SANttqpQ+IQIAWrQdrMf6ggU9j1/YbhWeJu58p+LUiAcO+5Mb771+/fh3bZugzGlMj0NGhyeujcSEz8IW029OG28BSpqiGUD11DI9l+Fp0Y7MKvK4Z9URplPp26aIIsg4eAHV2zH/39gERtJ0Y2WhXdVrFm2MeWT8FtAXkbrRlg8TnMrZtG4ZOiNhy5gTQFNo8l1EJmqbRypCiggd1+T1RbrhYt8Dq6u7s4+4Sit8MnCJriMiTHD9hRaqISEADDEBNk7z430XUV58g5bd/8dbTX/pi20w5w0A6a6NMBuuNyQu7BgTUyR+msp5TaD5CsQUAQ5UGIbRWe8nIDvaH1KC7DG6rQZncJ+K0hIW9JHnYJAUUiCmllmgA6Z86A87lauSBqFOPyMcTSQ2UV5eI4bf8vUYJYMUziLqJUNM0DTaYUrfp+tQ1Tauj5VVBIneFT41ajsHiRQsUxAUQMTUpk27o7O7D2wRrBbZ7NGuuVDFijGleBhEAEh8AgSoQpdoTAnXr9Z3bHy9mO01qzIuZs9E6LpKK5lJxi5GyTNk0kVKGiYvQiDkHjb8slAMAAFsQCKqZmGxaOMR6/Fi26itpLwC5SHbuhs2BQim1AqeGYeDqg6xSWDlZ54dwrWjEfmJCpJTaluc8Mso8OughadDyrICpTDnnxfVsPPVNBa/j6yFzBJdsBz7V1kDPSAi0/6OytSQBtnagSAKbAHBNGpH8Cwy3ES3jTUR0cnKyPjtb7OwkXWGHgLyjAwpec83j4DsZDYRPTnQUI2WdCHCDbyhn/kkeJ1/ej8FgaaKKv+iaOISA6wt1dCKYqUPk6BBADRBo5CH3WxovkfW55h+b/2QrDoidFIBNjaNkFomgads05LZpAajvh5QatlqA0AZLhE4BETQlhb8ZLUZTAFREG0INu6ribpgDwy5MkWT2bpTimszpY3c/GkEQhLUAgUNRL4no+Ph4tVlxpKo9jGKBoCvnIrtcPRVhBjNFUbRiUMZ0gFKSTKJLG5BJl2lx4iG07+kTGrPcIhAfayQjVTJkIgJcL1B+XFG1hwH+R3r4HwlTO5n0w7BanaWEk8mESAIsAGgdBuvbtTekGdiwkaVHrYElsiGOrmjMBAhh0ZZhW2GdVe0o+02VfMk6Byncy2oySEw90xrVs+tfJD5uuHt8tLO7O5m0kBLPUqjTASE9WvHPVhyj6hWttKZY2ZopYNPEt253pwjEjZtHTMUGN+jLz/z1/GASSQ7d8Rl+wwzq5YlGym4zgRSbB5DCPGrii7WH6NsQopolU2bg011T0/Loky6TBoJWY3gpRDAsawJQ2I0tHxFqRORCPt5GAUKKVvrppql0byazYlt0oZpxC7HgtVCwKEjSZuSXTPno6OjSpUuQUsCdQUzcmETRiUwrb4vrDvRW3cRiIFsuamo+Yh4wc1OKR0D7y1TCecpJpzbFGAfuMoEKxxECFdvmxqBJqYGj0krY7h/YjpUVM6wEKTVNM+Q8DDm1SWQvYWtGUh/BlFS1GRiBpu3VPlsXSffgY4UzRTXyOdYgKUDni7VQgKJUuwEdn5QyLDBmhNSiYxZsdPnyZY0lAz2KZYGlZFh3fZh1P/2ikFqLDPXF6uDsAZORYumJM0zfSkHxeFNPKzGwAEXxBEl5HToLClKwKKpjJuUgku6GVNwf+yOcjFGC/SqL1FIashbBihxpbp7KxboiEUjkK6wLxpmFJ5USz5REkC7TbSLpuQyooJQnlGlLFcZSkcJ7iXutfSKVb8n6sHyvVqvDw0PUAyZ9FzmRT+WIQDz2yRA8HZruep/VRklfZC2yUwyUAgRZ0tpxb57wRme5/mdm0ooigwqxX+CxZNM37rCX7NhOYnzVJAHV6GICWW6GZTekdxazKpYtMzgEPIkmM2iYUJJYCj8R2Bsh6CokF5ZiU8cCG+lgKsCk/CcwIaq7rGOo1AIDMYt7tU0Virh9DahvDLNsBGdnq3PnzunppIg+1RaCeVuGgSqPFKStNIIAoJuOQdAZac2+uFk07OcDsc5WFFOuos/KiTsrO4xYJed0CjmQUe1TQcPRR7RIJAzDVaW0KnWQe9SVVLKrgwE7akUTAWUtlYEazqgqcgV1aqhoyd6NYBuTqNcjAvE7ZDCJABpDBvcDW/XvspdgGK2Y7NIvsBBwN3KG9Wazt3/gei2tupRw1y1lUlpFsCBOXuC2WBEcmOgZz8SJeGKAWHY4gZkBIMvUgeqDvdOjdou8CqqDm/zwVJEd4AlcEququVkKRXk6pGT6RkC8BQ1yfRb/7Iurik4qPfgURhA/B0AEbcEvELdZIUUFelJVwjYIZczAuTJE3a+UAAh0CzVzDrJ82E6aLKLT8HGC+hURbvE7ZL6NhHbeTbeRfd8vFgtEzMJdNLegVOdB1dG12STjXPAspHRSg6qj0NCdtvyjz6LvMxVGZ32qe6Fj1t+sJ6jBoQ1GYSfvZGXkZW+ttbbo3UlyPrS07VbQ/UOkqoai7DhlftODO+Dd1gBAdgVl3VEBLL2VaBt5GoLEf4MvYwgsIQkpCAbnNyjQ9Fq7+xgmHQdY+YJ7TZl5QtNa+x0RATNRnkxaqaXRs3h1SMlZnxWiBixfuGxFIRb9mmeVEix5sRyXoibJPzxjmih53AJKT8hq2NQDinW0DV2Y7DIbYzNxVgaiMkMAmPUH/jNy2iArhlUbADKNImJGYCtk+EdA3UMiRHOICbHhczrZ300mk2LVrCwYAOufG1HRgxROlrSf3bJuE4gCxYjzNQ4FGarBkH2Ct7K8oDZa+iJ7BRFxaEq8aSGoC4o9jM8mNMNTCROE2ejIBtdPU6Hx+PURS5vUOhMrlERYowzaDAy7z+wlphyw2EZAnBMjylGNPROw7dVKq+ILWVwh3RMfpZ4/4gf7czabyVStFHKJB3H/P3pzFimILEhpyFEMvAvFrTrzR4gIDcpErxgqxJLHXmlZmEZm5kC8hbJGWKUPTJgQmpSaYRjSFHzzZMvLa5aKX8xyF/lnNBI1CWZJ/YzSHsWjmDrVFCOp/QgiYv4IRQqRrADRwH3RIQlgOXmbzTZqS2K1WoGy2finxA/5pNpNVywL02IJBGKDigXfJLtHEHtGbLCdtcnsUFLssA2xbPmopwPSZa+kU6TRipYUVbvn/6+/GRwJE9DlHdX/u5xW13m6YTKZrNdrNqGgZkyfkvaDKfFBRdtTQXIzk/yHrV9Wd6HiGpooXBWPL5pz0gDcLYfVepteFXaaL5iIx16H26CyKinVpEbFQ2NTYY5b54cLQ2aDyznz7rHi1Hwrf/L7rDtqzOvo1L6k4DiLDEsYiROWiHImIIJBVY7YyMVmQQe4xU0KXsbsrEMp0xFFyJhgNput12sCIszOCNwi3ZHc44r3wU7axZqdqKtNw5iL27Z5klTWQ5NCTBuyxoD6n9UPKhkbACe+vtYlWHQ7F8T3O+LoEAASQBP7bH23oGgIP0Y0klLDXJvNZqnkVLELB3dRY7+aRvFOUw0Jb8edin6BexnbsRhqqy0sVE0SN2xYSF9PUnhhqDwtFvPT01MS9xUdSr1Dg72kUk5Tra2wohK3jGVBeCFetelVvWUTSWH0ABZ4y1ynW67y5WUntd9sY+LGh+G9Mr4tw68Am9LKw/7y9pSSldclbGazWUuUNAh3TgeD6cjUjYRVSSnU4J0ImSxK31obInSlgNf4Emz74Cga5yHp3Ix60SrhRthgWix2Tk9PSaY5ncyG/RAdVoP1x+7UHiTznR6qyJhlVwOLFSkjNOFRjFUC9qLA+EozWVxTGCNab8TTGelxHBEKayRWk2SVXZS+G+xTwrIt9PDcKCDpaAo/qUYTCT5DxJSadjJJPJXKz4XeFEIQ/yTLXCnQI1VxMvu8TSrcr+nfSuHwp19HYw9Aoa4gs9nKG6dTeBXgzs7O3bvHJFNVFk2S/lvJi/WlgrYykVRpQqSRCIc3Fx8vCFHlmvVmCw9dzUrHEwmnkieULu8l4Qob6lF9yAgeqfGKN5FxmF+hIX/5LCFSSmJm27a1o7v52Xvihi10lI7zz6KO2lLR6S2ticETf8bEcHn1d0nwWL7MW8ewf5hxleOv/f39O3fuQK5568gyqmO4UtmMaETtikhjGD8Axj3HxiOmXGAg+wSmydY9GuODafy24YfNC2oBpkqAxw2gwhAPUGrU4W+qHpHvup96SqltJ7IxDQgLZYotocR6lTuL39W2i1xXZ6DGV27PMKrEYBgfqihVAU5cVCNXyJQvNinRaUq4v7939+4REdl286OXiGsE83X1bQByLoDxEtUhGmqwgDF89ZGF6wodoeQWemTa+CjMSpDOiii1efcFJQ8BAPFCK5C8AWhRgRoW0MXawSOP6Ab3+bAkaBlcFABE3NvfQ0yJgFJiG2jgTqFlsPBFEK5dkfEIprPMh9DKHrRlCiMwpCslwljMpsSRMX30DCQhUYh0wusEA8Hu7m7Oue+7/xeSkVHc1M6NaqSAG0t7p9Okxq1x0lMcswYicn+lk/yx1VRkmO4e2FEaNnO2zRCixG5YeNtSFO5HGQAFx84qtAUtiJcuXiKiVkoKEGWDlWAGEN0aqdFjSlhQwkVhGlqYYSyLwVQdI8biJDyKK6dcmaUgFkJ4UoePAECZUHZm4oWwWD2SmsViZz5fbDar2WKeUntvfZPq76Qm2QjMGeEY0rNVswVpXpmtlrGptC5qLxGSTM06PdmsYSz719qVsUAQAGDOHIonp42uNjEeV46YShMS2ywCHZ+exPJOL/bm1nIGIsIE01k7mU0BoCUuhyze6oZH24k2JiZkHZS4W9BZbjfXodP6qKMfsdv8TKH67myiESPRBgTQLXrEeqgtJAKixWKxs7Oz6TrKA6S4GxgZ7mZwEEYqcZ9uChrsq+ONUhMim8ECKaVUGEvSVUwFF6EA1IKJYqLcxN8wuVDXTJWrcdEdbWcr6BEelJM/ABz9SRwt/KAgXBrbNk1DRJNJe3Bwjh9sISYSNAQKf6I5CusAgVoRHRgjZd6jVOnrs9/BNjvF3LEJ1gOtLogmiSoHqf/YAhIAPsaMAHSJBWJCHLBJFy4ertdrG79TlzBT9qMJA5WDQ8mo2/xiMWFiDOba8bBaNrQUgJF8cvBw6i4lSOMNUkRDklWb1QIHIql6q79WS9TiE6ioo8QhsZcjL6ZeV6sD+JqprgyEz61P2Ewm+/v7QjeVGFDyUWhTfoz6yr+xxVF4hETkEJtMq621kUchH7TEj7LSGyD2wW4PbZmRyxl4ZwWRIdsICYAgATaHly/d/ujmMAxxr4gkfr3Ku7olDlbNr7tj0ptU9ImITNxHHaZx8MtTMBVfGXgYhhxXjchmQoyptlie0ntExDYaY7yO4WPsQJ01K5noXEXEYRhOT0/nizk3kthKMQNTWDBbdLNsC4A4b6zvq1yACgNsE3lC8LlSEr0jMCsk0QYkPu7VaGb/bynaGG1L+GlAEpEwXbhw8aObNzFTg7zJAWSAgTLBYB4GS6jrxI0EoFqP+ROXD5ICcM7tFhw2g60NWzgPDMggCFxYTxL7k6X8WZoYIye7M7zZlMe/b3kw2C272eZJx/fnnE9OTk5PTmfTBcjOqhQ37aOq85XjDB/lOJEfk4M4hCTN+PU6A6CxsoErs4bosyLaAqkHlfGC4AK0DiplAXjnS+sv4u7O7ma9OTs7k+0iC4ULAUTggd8WAIpFVyUNttgA76X/xS6Vw97yRpVgdzpAGtEhANY3K9EjpBvZkgDdfFDy01gZMHSiHE7N+vjIMAzDMMznc7uiMy+BYPpY5odZD2gsZb7KXDYazh4SKr5zdUQdOYEtDlPx1cSGHIyCxWopjTZD8KeuVuBFMVjQrhJNJpML589/eOPD5fLMdTGOdvvCwiKuUfoGspf32G38MbNUckWDkUgigzZEtm7C4hWrC4PRG7f2mTllt2mUGZXVvXawHSGa8U7hyEu7F+6HITXNzu7uZDIxqO+L2VzpUCGcggMTiAqTBvNN1caojJ3G47eZE/ThktGY7RFFIVJ9QrW9+o5M5NsbQLjTTCWmdOWhKzc/+ujs7Ex4U+m49a02NIX9D2JXPlWCVuvdditueQlTRlJvpn+bqurCgaJrVATnRTe2/RloXspu6NJ2ibyXIQQV1qZt54sFL+nnT1IpuaevTSmZa/fxqi2AYG8g7JxHINKiG2QHZ2NWTFyKec/wCj0rEFEhRaCnWqjCo5X0EWJceuBSPwxdtyFdextvdkXcSswt1OBRyRfY5rtH3sEtTAG3tkZJGmzfi/FRvrfKULS028ysxcVB1LdZO++/qLnwMeecENqG1xkle8oy6xWFmYfx/SWiCYotPwhldeLBXItQRUnALZux82wfIqQw6U18Vq0FB/yovoVS4H3NEGdk2tvb39vf67pu6Psa/BJxd7ecFVH7OxspaXVChhHnjPSCAiv2oEzLV10N0oDO0tJdl7dtg8mVrbKOqSdSCouTcE9tq/O2WVCWHg9fiIaBN+1DBN6OTR7xrcGVWkVzGPENRUmyXBlGvTQBj1JfjlZeED1XcNUlACt9BKpWh84iVF48QBkCTO3k8OLF05Pj9dlSD7gNtC45XVnvSAG9GpI/JS+3ebHtEHX8sYkFgCLQG3tJRnb3c53cNbMKMQ4IWheJX25XE1ipPoRfnClvNhveJivEyPKo2KWQPiKTJAFJnBchShqrImCDmLiQT2ZDeVVCDEbkCJ8UUuQjNSJMUpqYcwZZaToiij0o0sY7welylGAXNVU2iJUjIoDDixdvfvTR3aMjBCLINk1rjk/306khEf/OhlQvoiYstEii1KLxxyCtjcWuxxv8/pBqGlPM6qtq+oBNcvmLR3QEQwJCqy1dNrurnkXyEUAAaxGjZMdUWkmfYSM3t4qHUPGKdSy7RBPZiZO2ZHQYOLchcHxMOwBfdqk0wqgx9ozaNir8tL5cEHcurFP8HpPWO7s7i8XO3aOj4+O7iLqMKz6hSARHkwMQVqFElxGkwXx6+Df2yMxM6fu8cHmbXRkbJPsz3uZmqYD4zha38I7PojUvC7Yjm/RKnFSdTmd8KiYBYEpN4/VMiYiQp/glbkfgKCgzuUlcrGqjrMblrf45C6h7tm+JZYH3i48hRi6Ko4MFY1fn0EkpnjMNIZSVVosF2MELy+wECqACmkymj3zi0ZPl6c1bt0IohIlSgga2qWQ8VJqb9lhafyCAgcabKgjos1UroPsjyC/k4zLxsp0z/bryuxK4lJIxg4KM1tNVCJB08wAmqHXDfRbjBBFEe9jGk0B1TufmkJT5SJh4M1rRdtv6EyOqdp0ixUBhRinr3nLSJwLKSCTHI5lfNsZWH7zXn7o5R4AfSlWHiuQuCHKWVZqBjSPFJoLDw4vz+TyTODs1pWCuv/qMU1Hegajy0cUoKgSwRUCWevBxmiLUgHFkeEoXBfGl0lS4vsU/BeeFyrUclycQ+xfxXHpz7JkUifPjfMIzEQHJ5noQyoKLbfEgBsWlp5VDIEFpgRm1aLiBRATI+TITA92kgBdZKpqJEjYSJ91nIcuiuUa9EyLZzrgcZ9gZ0kkjWwLIRAPq3IYYAiIEbKeTw4sXT49Pbrz3waRpG97LESnDUFO/9D52qfqVVYtXj4KQjMNLkZ4MMCgnHUYpXUtx9JbDvKT/KlOhGsGoTG6HYmV/tXgcIAE2yIi2Ijong+ulntVnGIa+7zPvtU6yNUxAJpDYItnuaG51t3lilsHqYibOHKL9WjxInoUuYUnhl038AsHLG0NwYZgAASBs/IIKeIki2AdEvHDxMBPdvPnharVETZ/I6gYo7rR3mqpFn1JQN2YE1HN5L9H6bpssRF++NYzdRvBSwkDBUEWk2FVRvSK7uA2Yj6ydGf/qztVqRURU8i4+bhmk4q33G6FaV8+daB7AMJ29it82Hq1/haSrWoM7VQiuFIRUxIBxiUWw76DZBPf3wl0CmM3mhxcPc84f3rghDzGyzoNvMu041VWNXIzJ5kdrfiQpHM1e/47qEQCVtqFjAjjGomCQqOLTvexE/Cn2cEugVtrU8Sui447P5Zy7zRooJ5CjE7McCO2pNU9EjkFCDE2qnpvSE2l9A3lkFwkUxz+mGlk0WdGlAFeGKaVXIkBq24KdKhoh0nQFYUoNI6SjoyPKGYsHaprL+0hNd00Yx8IUPsXAGcaBVhRte9wRSWRqGZdttRn8r6H+sUBAqbz3WakxfnDUJUFFtdBjsaFDkmlR0llvtYsAZLtjjQhhtHURQLBt89WSozGIUwOUaQAqU0NSwkuKKHIldhgrxDVG4a+qNRT+q2gRKAkwny/OnT8/DP2777zdIOhyZAyN+P1jF+DEDTCZFJRFush/VLQigoMlqsGCi1tfOv6YEMeLwUKXvQ1AihSPm+CX0u/fwyhotTrjtAbpWRF8Z2psoyLinVYIgmgEEm3/lN7ElxqSOgqL7kKPjOZxIkXm9h2bKQhVoKykDKbXgKaAkTKg1nusfVcLTM25cxd2d3ZvvP/+8vTEAnLQ1Z7+Vv5THWs9fLWNVgHkFCEpYQuVlQGygFlIApvfUbu+jcjFF+dxJSXjTppmjBYrQonf/OLYaBIgYNdtNuu16LNcZmQGVtqGvtxIvYSKqaMTedMIdokAK/xBRKCsiEBDJf/i3rMcP1oKz2iEBieCPqsmBW6q44CQoHD6oOEU+4Wm8/nhpcuLxeLdd97BoHwm6QUZ3ecFyBiRr42o4qLyh/+NSl82Poag9/uMLVB4ScEslVcCKOFBbQxrESx6g5Apn52dYTkgUDpYQouIkhxlgiDnDxckcmNY52f4PTohQ/wOyuDMjhYr8dKParT+HQB0vjBJnRoRDZHMKrWMV9QPs22rZyVl+3WCYO8k1w57+3uHFw9v3rp5dOd2i4JLZbwO83mNuvPZgUggeUJMQBYwR1bxg1mNVk06vkd/jMjDaW7vMWJtM43G98pQhRsKsBVnWuINFXdYHvq+67pOc54huOIzrqDMG6GXClH01qEH4qHADaDLmq0miomsQDHb4xris94+Jl7go2MjVAuJiFjG2Igiu+7CEGDbVBxVuqgPpKY9d+HwwoULzz37LA09lvq69YOpSCUbwXkoyjl1bmoekhbJoGaivRBWOHVP4eAByOt45lJeZi+6J3KCsUwoQX4p9gpXMOe8PFt6a8pasXOio3K3l60xCQjjSkNOcmZELZ3XSIGCFsj0Q0JLisbO5ixHn8jtaSvhkjGT2A4x3N5GIwqv5ZPUWNwAEmFDlcmALDlGS5clnC0WV64+sr+3/8brrzdNw6OTozGCgUSNQY3fJRsqzMFGjyJxUbKustgtyqL97ssQYjoAyrvEqLJJYAPmd8bHZdTRhJefeM/4XW4Xibr1pt90sfoAiIt3GG1k0tl3dmrQNA0h1imRUcRkPeZVCqRhedXp6in+OzWIfjR2yQXxfrK8kOxUPuOobsPMnyY1iJgIkuAtU1VuDEGXf9T88GHh7u7u9cc/8/xPXlgtT5C8hBItLDA3GvqKZS2YyZYPmmJqIoBrXTkcSeRGHetGqxNXSfFy+NvfQZFW2+D2veyWKmSt2IjYD/3Z2VkKk31MOt4BsWkaK33kIci2w9ZCWLZh33l2Qp6KPWa+J8yAGQH6ftDTmeTDpghRIIDvuB28FgIgZCDZjQlTw6Ugzp1tDot4ty1NeSJSQkqyzZDnoGVE7DQVDicAbNL++XNf/tVf+cH/+uvN6gxBMs1EGkjyajbkDJPbiTFDsNhdqTDmRLw9YwJIWwtWlZgRVwEA778d5hWo7AIW55XHLT+2pIgQeJoIRjsbmZOiwmRA329OT08GyhVE4em0ASC1TTuZCo/EFyAAgRUcIVQVpRhJtwVHGHEpkAqRFJZqDa42l/wpbYByDhhUvX5tha0LcnYK9P1gb496r+RgtW7qgicZJqSmuXLlysMPP/ziT356tlrylArlKlWN+tLa44x6p0a16nLN7/gLmj0DEcd69YjyGHV7HAzCpndoY1E541uqXhkdokVQcqVhyOv1WpA1hpcI8kMimM7me3u7sZ9JbSIy8x1GWRl/6FbYdoDCNQTS+SZdT2KyL8GUklksoOq4vQIUWeRcGvqx/1aqrFZnMUE68snM+IEoF+RQw4OEbTP55CevYUo/f+Xlk+MjhKxrVEDDrxSZH5mxFViMryOArpAr2rm/o7FMm0latMl16KNGSG0Xw1kRLNI0cu25yAeWPTCnbrNeLc84iIwiK5wCIILFbLG3ewAq+iSFIkSNeGNNi9k8a3S6Iuy+o1AYPLLnEndD2VyyYAuqrYWPx96iDrj6uaQyqhRA3/dku1oHvzBmzRYGC4lxZ3/vkU98cr3efHjjg/Vq1aQEWDypTdf6Pf7YAKNB0HlpeeWWdgKQCtiRHJ8F1z5CZmD1DaZGrFAeShs3w0ux7EtS2q/Ozpanp1kni2g0h0hEs+l0b3/fFqlJz6WhkX4wSeKkkYR5ZbMCBpAAcKCBh8MWM4w9JhJ9DZQ3C45KUmRaKa1+M7DvUy1Ua2/5cTPuEnjWI9MNgRAgNecPD69cffjk5PS9d99dnhw30m97DgHCTiNBHIky6kyOxWiF+iquQeSN3LM7R1L2awbVlo1C/UH3mMEBKX/GIAmj/1SfZ0hrBJ4A2KRtVuuz5bLrukBSs44uBgcH5y5cuMAzIa2yKYYAAAz0SURBVNaa4O3UNKBxCVkKoBoNhlkHucKpcRFwIiDgwBvZKKIYXCI+gtNAqlUNku604MUUJoC+mRUE94cg4ViTfBcGIikSZ7boNtVMCAqq546FdK+bpmkvP/BgzvnWzY8A4MrVq4vdnZy5l8kkVVoq1EhEkdQze/dLJgHvzCEYnBRkuCHSEfjWNja0+KV6vQwkegW7GaPEqLGvMnZmnAA269VyebrZbOIN7KnUdBARTCaT/YP9xWLHBQwRTIzmi8XqbJVz5/EhglENLdBVUx3ESRgLCXOGrqcupyY3kFJjDolYbRrVwrJWRvfasrOGTCm3LMcBkA0E7I6CFwIplOMECMlPp7S+MnEsh4mT6eSBBx/KmY6OPv7gg/cfeuihxWJH0gAEAJAx44iJOgqFA0qeikl2T+UetFcoADb86S7R2tcRotp2CNAEVRDtImz5WG1aNjoAQEq4Wa9PTk7W6zWFLT1A0QDTnAhyzhfOndvd24Vqlyailr/t7R2cLVdDP3iqUHWPO8gGJiktCRLooZ4JYQBAwE03fHj7aNnhbD5t26bB1CBwxfZk0hJi3/UJMCHM57NMQ84DACTEpkltSoCQEBvbNgORNDGOhjxk4T5vz+XZCu4XjjQTy7IYC4UQASGBrmQgwOlseuXqVUS8ffvm0HdXrz68s7cvcowIkLMUlUbnFraqEU66tASoNE7Ec6d1Fa0QNJutEo3w1IHYDAIAr4MNdVfInN5SyxakyDAAmGVHpK7bHN+9y4VpNtvqvWV2Z6Aki9l3d3agFFZE3bNsMp3sHxwMfb9ananNwBSBCPiaHVVi1wYiSglzpp+/8VaG9wCxaZp20k6apmlwNp1ePjxcdZuT02WLzWw2efjq1fX67PRsSQSzSbu3u7OYzZerZcLUJJjPZtPp1MpcJpO2bZPuOYGIQIBdpqadiLwBmDgowSEoX0lMVTBQF2uyMplOH7pyJSW88cH7fddfe+zT88UOBwuJZDf3qujCTf4IdiiP/Re0UgrN/5RcVmwQDQtfp9g+K4xcEcQMEsCXQoBaWmgISW63W/q+//jj26ulyxAUmRrUXgACPPDgA7v7+8WQFJX4Muz9g32CAY9gs17TkHPWsnRJ8IUVSIxP3WGLSZ1MJseny9WQAFvkwCNhApjPJtQsbt2+fXR8gqlZTKc02fnwoxu37xwPmaZte+nw/GKx+MWbbxFAgvzoI1f39vZufPjher2etO2DDzxwsL+3PDvpur5t29lsklKTKfWUMDUCgG2qX4UkzuWpersYIYIfbQ6elJjMZg9duTqZTt975+2XX3rpqaeeatpJjqZBpUiNjdCEdxoVeyBI33avkqlfLaKRIzWN8pZikP9DXXosiRINTeXdaBnclFSaFT+IxBBoQaLXwIgXkXBbMgEf3fhweXY6PrMQ+SwHXV5NCNP57Nz589Pp1AQrqk2rYkEAcHBwbndnZ7VanZ6cnB6fDMNABMST27p+CHmTp4BICAiREvbzWXt44dxZTkQNprZpm5QwDwPmvDxbbvohtQ1QGgBuHR2dnK06Amim1Lan6+501a+oyTknSMcd3b159/ads7PVqm1SO987Xq7fe//9Pg99HqZN27QtZJi3QyeJbOZOZdGVwoq+1Pyg3O2VIQRaVYCYUtseXrw4nUzefvPNv/27v/vVr/7qYnefgAbIAMltgrqyMt8ZV8iZ5fdueUn+NghFalhwvHsaFO24BCpHS9QGAEA55Pb8cbGReaC33nqz6zqC3CTg2VVeFFusLABCwjwMB+fOTWcz24Eu9oCI9IguDiWClNn5ykREOffDhoiGYcj66fth6HPOw5D7bjNkwN2Dy//9z773/q2jTZeJoGnalFLf98vlcnm6nM0XhECAKTWb9RkRYTODZpIS0pC7rp/u7LEWNgBI0A+8ZyPtTFuk4WR5miYzaFpJe3fd/gz+ze/80ysXFkg9QGkwIZcagzJPRcQZfTGxKlRgm3gSyLlKROvV6oP33n3ppy9+7Wtfu3jpUk6y1UVWhQ7ULHjMts6NvzoVRJQ8JBNYHiO1IAl0v68MwhT1t5h1rWmI9oN7rqTI9IkMX2q0BASQT45P3nrjLb6taRpM0DQJUpN47pM/TUJGq22TJtOnv/jF3d09U1bDYfzqlkOzLSsNbN8CBGzSpJn6T+brJZTjXHLKmP7dv/2Xq56GgYYMlDFnGoah73OmPPTD8enp6XK9Xq/z0J2cnt49OTtdrjbdmobh7Ky7+fGds9UGANrZbDqZ5G49DEAA7Xy3oYzDOk2bpmnm8wUQUT/M06boM0JCZNgeCMp91SRHQe8YAQUp0vHNpvNHHv7E/u7em2+++fbbbz/x5Oen81kuVomNmiovRMtklBuhYIadxUycBZcei0UoFVBS+GuLrxEhlmgAUsL1en3jg/du3rxJBC20TdMwdTJkzJQS+YHEQE3TQKKmaR7/3Gfn83m0rAXAB2gRoxILahNrpudLIiLIyRuA6vf1OhEkKRYCOFhM9zi+B/ElBIoIAIZ8kDNxMJhz7nnLUSIgyETdMPBvmw1uNt1yuTxbb9brDRBt1us7d4/PNv3papUHyMMw9BschhaHru+AekyQADWTJCA1sCsDYdbj21DhUOCwnC+gu01wTJYnk8n5S5cms+lHNz589ZVXH370kb1zBwooic2fbnRYInmAYIE8ADAyxvswRpjmCgooBkZ0iXVGbzO0J0ZBpN2hYUpwenr8/vvv37lzB4gQ/cxCIsTMyHsYKCNi06SGCDHt71+4/vhndvb2U2ri6yqI7aF/MFaCClmM3HMHqY+SCKNBBQoQgMJfRnpErv0mmIJiMgISpJxhyHkYhiHnIWcgGnLuh2EYcjfoarucIdOF/Z0WCWgAhJxzt9n0fU9DHoYuZ8p5yJkyULfpKOemaZqmkRCdLPas+61YlYE7x9G0Xi6P7tz51KevzRc7p8uzzaazBzRCd5o6rSp+hwSgkgYAiE8L5BXpKZB3JCX2HWuTI//PYtQo+V02Z7PpwcF+0zar1WoYBkG3Sdf5G0P0uZwz5TzfWczn8529PUmCi9mgyuyh20zE6ucYJVZCE+lVjASlnjWKUeFFtmU17O2ml9YCVfcBWNLDUxKMKxAo05CHPGSgnGmgjAQDZSSk3GfOpefMJbPQ970EV8Ceb9BImYC4rEb+lwGIcu762Wx68fLldjJZrdbL5XJ1dtZtNmHogqYN11siPcLqWgLkBwLETByu1PvWOaIfWaARIbMFdHJ0MULTpMXO7t7e/mJ30bZy3i1BggDaqncBAK+kbyYt5zjs+pjpIjmVdap/Lk3RfeRg/P1eD269JwI3//P+dOOBVY8r0B4nnTNRHjKLERu58CKRM+kwRVxDRERDni8Wk9kUESnnrus26/XZcnn36AgxcYDDgkgWxxKq75Td/cOoKQZraDP4GlJGaRMkHpKYlSnSZtW6QWI7mxIudnYWu7vz+WI6mzdNUs1W0jGptwSA1sstxqISEmmq3Hfyno/VL6icmluU0L+RIN7n+/3vr1691c9uba0YWshTouUdESDkeLSRe5A2SCYBbdabu0d3h67bbDabzZpsOlzFSF7uHfEUA47ECACoPFtCX2SAjrtQRIglI/h7atp2Opss5oud3Z3ZfI6pEXmoa5vcmQmcUg+ylfL34gLYEMdyMGbJ/U3OVnxwfyP0Sz+1HJS9glJ6YCTxYyG7153jTo5JUf2JiIwh1ut1t16fLU82603XdXoEKn/8vMPMx9OjiC0WMiRG1GTLjTE7++DMsQB0ocgdEQBS0+zu7Ezn89livrOzoz8mvIcAjfHML2XZdmxk1qhSgq1e5l4N2SP/fx26z8WtHah+gpGgjMc5VomxjG61bdWV8aiBZSLn05MTLrTo+q7rNnmwmfyMCHkAREk8Y2jnPgqpVwSJAwAg8c5JmTLLH9/PxdFN287n88MLFzDsEYu2Id49qLqVm1tpVQy5ai1ao62DuT8L7yMr95L0+5ilX2qx7i+m93nF/Vve+vh9DFKl2Y4wiLquOzk9Pjk57jYb9jkDF/GRKK12o4Zu8Y2liMs1/jchAEEGwsTHW6TJZDKbzebzxWKxM53OrLJlK/CAe8vQ+Avcw3qNKbYFYm8byXYKjsdf9eCXcug+jKlajnduJcfWG+7FJ/4byoxD9anu36qU96AJAUDXbU5PTpbLJe+8OWyGpmkQwNY1kCUFJAViUYFh4PBV+cgPtm2LTdrb3z84OJhMp2DAaRut7kPze0nM+JH7k7RwamMhgFKG7PmxhG41APeieNX+LzVp456MxwOjz9ae+3VEbh0A8N6qWb1iq8eEku5l/3Pfd33XUT8cHx8vT067ruv7PqXUtA1w4ga0cpek+DNDJoREqKeGZiCaTKdt2yLi3rmD6XQ+mU4Rm60kvRfxt9LzPqTbqkhb6fB/Af1a51sFMStqAAAAAElFTkSuQmCC" }, "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/fixtures/template/ref_configuration.yaml b/tests/fixtures/template/ref_configuration.yaml
new file mode 100644
index 00000000000000..f7731697c71766
--- /dev/null
+++ b/tests/fixtures/template/ref_configuration.yaml
@@ -0,0 +1,9 @@
+sensor:
+ - platform: template
+ sensors:
+ test1:
+ value_template: "{{ (states.sensor.test2.state |int) + (states.sensor.test3.state |int)}}"
+ test2:
+ value_template: "{{ 1 }}"
+ test3:
+ value_template: "{{ 2 }}"
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index af01163bfd5b08..001c59e2f2c883 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -5,6 +5,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition
+from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
@@ -421,7 +422,7 @@ async def test_state_attribute(hass):
"condition": "state",
"entity_id": "sensor.temperature",
"attribute": "attribute1",
- "state": "200",
+ "state": 200,
},
],
},
@@ -434,7 +435,7 @@ async def test_state_attribute(hass):
assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": "200"})
- assert test(hass)
+ assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 201})
assert not test(hass)
@@ -443,6 +444,31 @@ async def test_state_attribute(hass):
assert not test(hass)
+async def test_state_attribute_boolean(hass):
+ """Test with boolean state attribute in condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "attribute": "happening",
+ "state": False,
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 100, {"happening": 200})
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 100, {"happening": True})
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 100, {"no_happening": 201})
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 100, {"happening": False})
+ assert test(hass)
+
+
async def test_state_using_input_entities(hass):
"""Test state conditions using input_* entities."""
await async_setup_component(
@@ -807,6 +833,7 @@ async def test_extract_entities():
"entity_id": ["sensor.temperature_9", "sensor.temperature_10"],
"below": 110,
},
+ Template("{{ is_state('light.example', 'on') }}"),
],
}
) == {
@@ -867,6 +894,7 @@ async def test_extract_devices():
},
],
},
+ Template("{{ is_state('light.example', 'on') }}"),
],
}
)
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
index adbcca63990f3d..2bb993f1197213 100644
--- a/tests/helpers/test_config_entry_flow.py
+++ b/tests/helpers/test_config_entry_flow.py
@@ -238,7 +238,7 @@ async def test_webhook_single_entry_allowed(hass, webhook_flow_conf):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "one_instance_allowed"
+ assert result["reason"] == "single_instance_allowed"
async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf):
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index 1513d573b5630c..15e9bf55aa451b 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -114,13 +114,14 @@ def test_device_class(self):
assert state.attributes.get(ATTR_DEVICE_CLASS) == "test_class"
-async def test_warn_slow_update(hass):
+async def test_warn_slow_update(hass, caplog):
"""Warn we log when entity update takes a long time."""
update_call = False
async def async_update():
"""Mock async update."""
nonlocal update_call
+ await asyncio.sleep(0.00001)
update_call = True
mock_entity = entity.Entity()
@@ -128,22 +129,16 @@ async def async_update():
mock_entity.entity_id = "comp_test.test_entity"
mock_entity.async_update = async_update
- with patch.object(hass.loop, "call_later") as mock_call:
- await mock_entity.async_update_ha_state(True)
- assert mock_call.called
- assert len(mock_call.mock_calls) == 2
-
- timeout, logger_method = mock_call.mock_calls[0][1][:2]
-
- assert timeout == entity.SLOW_UPDATE_WARNING
- assert logger_method == entity._LOGGER.warning
-
- assert mock_call().cancel.called
+ fast_update_time = 0.0000001
+ with patch.object(entity, "SLOW_UPDATE_WARNING", fast_update_time):
+ await mock_entity.async_update_ha_state(True)
+ assert str(fast_update_time) in caplog.text
+ assert mock_entity.entity_id in caplog.text
assert update_call
-async def test_warn_slow_update_with_exception(hass):
+async def test_warn_slow_update_with_exception(hass, caplog):
"""Warn we log when entity update takes a long time and trow exception."""
update_call = False
@@ -151,6 +146,7 @@ async def async_update():
"""Mock async update."""
nonlocal update_call
update_call = True
+ await asyncio.sleep(0.00001)
raise AssertionError("Fake update error")
mock_entity = entity.Entity()
@@ -158,28 +154,23 @@ async def async_update():
mock_entity.entity_id = "comp_test.test_entity"
mock_entity.async_update = async_update
- with patch.object(hass.loop, "call_later") as mock_call:
- await mock_entity.async_update_ha_state(True)
- assert mock_call.called
- assert len(mock_call.mock_calls) == 2
-
- timeout, logger_method = mock_call.mock_calls[0][1][:2]
-
- assert timeout == entity.SLOW_UPDATE_WARNING
- assert logger_method == entity._LOGGER.warning
-
- assert mock_call().cancel.called
+ fast_update_time = 0.0000001
+ with patch.object(entity, "SLOW_UPDATE_WARNING", fast_update_time):
+ await mock_entity.async_update_ha_state(True)
+ assert str(fast_update_time) in caplog.text
+ assert mock_entity.entity_id in caplog.text
assert update_call
-async def test_warn_slow_device_update_disabled(hass):
+async def test_warn_slow_device_update_disabled(hass, caplog):
"""Disable slow update warning with async_device_update."""
update_call = False
async def async_update():
"""Mock async update."""
nonlocal update_call
+ await asyncio.sleep(0.00001)
update_call = True
mock_entity = entity.Entity()
@@ -187,10 +178,12 @@ async def async_update():
mock_entity.entity_id = "comp_test.test_entity"
mock_entity.async_update = async_update
- with patch.object(hass.loop, "call_later") as mock_call:
- await mock_entity.async_device_update(warning=False)
+ fast_update_time = 0.0000001
- assert not mock_call.called
+ with patch.object(entity, "SLOW_UPDATE_WARNING", fast_update_time):
+ await mock_entity.async_device_update(warning=False)
+ assert str(fast_update_time) not in caplog.text
+ assert mock_entity.entity_id not in caplog.text
assert update_call
@@ -671,7 +664,7 @@ async def test_warn_slow_write_state(hass, caplog):
"Updating state for comp_test.test_entity "
"() "
"took 10.000 seconds. Please create a bug report at "
- "https://github.com/home-assistant/home-assistant/issues?"
+ "https://github.com/home-assistant/core/issues?"
"q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22"
) in caplog.text
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index 821285cfbe1ce3..bb0d17d7b0e3b3 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -14,6 +14,7 @@
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.event import (
+ TrackStates,
TrackTemplate,
TrackTemplateResult,
async_call_later,
@@ -23,6 +24,7 @@
async_track_state_added_domain,
async_track_state_change,
async_track_state_change_event,
+ async_track_state_change_filtered,
async_track_state_removed_domain,
async_track_sunrise,
async_track_sunset,
@@ -255,6 +257,142 @@ async def wildercard_run_callback(entity_id, old_state, new_state):
assert len(wildercard_runs) == 6
+async def test_async_track_state_change_filtered(hass):
+ """Test async_track_state_change_filtered."""
+ single_entity_id_tracker = []
+ multiple_entity_id_tracker = []
+
+ @ha.callback
+ def single_run_callback(event):
+ old_state = event.data.get("old_state")
+ new_state = event.data.get("new_state")
+
+ single_entity_id_tracker.append((old_state, new_state))
+
+ @ha.callback
+ def multiple_run_callback(event):
+ old_state = event.data.get("old_state")
+ new_state = event.data.get("new_state")
+
+ multiple_entity_id_tracker.append((old_state, new_state))
+
+ @ha.callback
+ def callback_that_throws(event):
+ raise ValueError
+
+ track_single = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, None), single_run_callback
+ )
+ assert track_single.listeners == {
+ "all": False,
+ "domains": None,
+ "entities": {"light.bowl"},
+ }
+
+ track_multi = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, {"switch"}), multiple_run_callback
+ )
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+
+ track_throws = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, {"switch"}), callback_that_throws
+ )
+ assert track_throws.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+
+ # Adding state to state machine
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert single_entity_id_tracker[-1][0] is None
+ assert single_entity_id_tracker[-1][1] is not None
+ assert len(multiple_entity_id_tracker) == 1
+ assert multiple_entity_id_tracker[-1][0] is None
+ assert multiple_entity_id_tracker[-1][1] is not None
+
+ # Set same state should not trigger a state change/listener
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert len(multiple_entity_id_tracker) == 1
+
+ # State change off -> on
+ hass.states.async_set("light.Bowl", "off")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 2
+ assert len(multiple_entity_id_tracker) == 2
+
+ # State change off -> off
+ hass.states.async_set("light.Bowl", "off", {"some_attr": 1})
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 3
+ assert len(multiple_entity_id_tracker) == 3
+
+ # State change off -> on
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 4
+ assert len(multiple_entity_id_tracker) == 4
+
+ hass.states.async_remove("light.bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert single_entity_id_tracker[-1][0] is not None
+ assert single_entity_id_tracker[-1][1] is None
+ assert len(multiple_entity_id_tracker) == 5
+ assert multiple_entity_id_tracker[-1][0] is not None
+ assert multiple_entity_id_tracker[-1][1] is None
+
+ # Set state for different entity id
+ hass.states.async_set("switch.kitchen", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert len(multiple_entity_id_tracker) == 6
+
+ track_single.async_remove()
+ # Ensure unsubing the listener works
+ hass.states.async_set("light.Bowl", "off")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert len(multiple_entity_id_tracker) == 7
+
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+ track_multi.async_update_listeners(TrackStates(False, {"light.bowl"}, None))
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": None,
+ "entities": {"light.bowl"},
+ }
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+ hass.states.async_set("switch.kitchen", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+
+ track_multi.async_update_listeners(TrackStates(True, None, None))
+ hass.states.async_set("switch.kitchen", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+ hass.states.async_set("switch.any", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 9
+
+ track_multi.async_remove()
+ track_throws.async_remove()
+
+
async def test_async_track_state_change_event(hass):
"""Test async_track_state_change_event."""
single_entity_id_tracker = []
@@ -668,7 +806,7 @@ def error_callback(entity_id, old_state, new_state):
hass.states.async_set("switch.not_exist", "off")
await hass.async_block_till_done()
- assert "lunch" not in caplog.text
+ assert "no filter named 'lunch'" not in caplog.text
assert "TemplateAssertionError" not in caplog.text
@@ -789,7 +927,6 @@ async def test_track_template_result_complex(hass):
"""Test tracking template."""
specific_runs = []
template_complex_str = """
-
{% if states("sensor.domain") == "light" %}
{{ states.light | map(attribute='entity_id') | list }}
{% elif states("sensor.domain") == "lock" %}
@@ -810,7 +947,9 @@ def specific_run_callback(event, updates):
hass.states.async_set("lock.one", "locked")
info = async_track_template_result(
- hass, [TrackTemplate(template_complex, None)], specific_run_callback
+ hass,
+ [TrackTemplate(template_complex, None, timedelta(seconds=0))],
+ specific_run_callback,
)
await hass.async_block_till_done()
@@ -1024,6 +1163,8 @@ def specific_run_callback(event, updates):
await hass.services.async_call("group", "reload")
await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
assert specific_runs[-1] == str(100.1 + 200.2 + 0 + 800.8)
@@ -1106,6 +1247,7 @@ def iterator_callback(event, updates):
hass,
),
None,
+ timedelta(seconds=0),
)
],
iterator_callback,
@@ -1133,6 +1275,7 @@ def filter_callback(event, updates):
hass,
),
None,
+ timedelta(seconds=0),
)
],
filter_callback,
@@ -1141,7 +1284,7 @@ def filter_callback(event, updates):
assert info.listeners == {
"all": False,
"domains": {"sensor"},
- "entities": {"sensor.test"},
+ "entities": set(),
}
hass.states.async_set("sensor.test", 6)
@@ -1259,6 +1402,268 @@ def refresh_listener(event, updates):
assert refresh_runs == ["static"]
+async def test_track_template_rate_limit(hass):
+ """Test template rate limit."""
+ template_refresh = Template("{{ states | count }}", hass)
+
+ refresh_runs = []
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ refresh_runs.append(updates.pop().result)
+
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template_refresh, None, timedelta(seconds=0.1))],
+ refresh_listener,
+ )
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs == ["0"]
+ hass.states.async_set("sensor.one", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0"]
+ info.async_refresh()
+ assert refresh_runs == ["0", "1"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1"]
+ next_time = dt_util.utcnow() + timedelta(seconds=0.125)
+ with patch(
+ "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
+ ):
+ async_fire_time_changed(hass, next_time)
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1", "2"]
+ hass.states.async_set("sensor.three", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1", "2"]
+ hass.states.async_set("sensor.four", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1", "2"]
+ next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2)
+ with patch(
+ "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
+ ):
+ async_fire_time_changed(hass, next_time)
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1", "2", "4"]
+ hass.states.async_set("sensor.five", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1", "2", "4"]
+
+
+async def test_track_template_rate_limit_five(hass):
+ """Test template rate limit of 5 seconds."""
+ template_refresh = Template("{{ states | count }}", hass)
+
+ refresh_runs = []
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ refresh_runs.append(updates.pop().result)
+
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template_refresh, None, timedelta(seconds=5))],
+ refresh_listener,
+ )
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs == ["0"]
+ hass.states.async_set("sensor.one", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0"]
+ info.async_refresh()
+ assert refresh_runs == ["0", "1"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1"]
+ hass.states.async_set("sensor.three", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["0", "1"]
+
+
+async def test_track_template_has_default_rate_limit(hass):
+ """Test template has a rate limit by default."""
+ hass.states.async_set("sensor.zero", "any")
+ template_refresh = Template("{{ states | list | count }}", hass)
+
+ refresh_runs = []
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ refresh_runs.append(updates.pop().result)
+
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template_refresh, None)],
+ refresh_listener,
+ )
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs == ["1"]
+ hass.states.async_set("sensor.one", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1"]
+ info.async_refresh()
+ assert refresh_runs == ["1", "2"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1", "2"]
+ hass.states.async_set("sensor.three", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1", "2"]
+
+
+async def test_track_template_unavailable_sates_has_default_rate_limit(hass):
+ """Test template watching for unavailable states has a rate limit by default."""
+ hass.states.async_set("sensor.zero", "unknown")
+ template_refresh = Template(
+ "{{ states | selectattr('state', 'in', ['unavailable', 'unknown', 'none']) | list | count }}",
+ hass,
+ )
+
+ refresh_runs = []
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ refresh_runs.append(updates.pop().result)
+
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template_refresh, None)],
+ refresh_listener,
+ )
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs == ["1"]
+ hass.states.async_set("sensor.one", "unknown")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1"]
+ info.async_refresh()
+ assert refresh_runs == ["1", "2"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1", "2"]
+ hass.states.async_set("sensor.three", "unknown")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1", "2"]
+ info.async_refresh()
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1", "2", "3"]
+
+
+async def test_specifically_referenced_entity_is_not_rate_limited(hass):
+ """Test template rate limit of 5 seconds."""
+ hass.states.async_set("sensor.one", "none")
+
+ template_refresh = Template('{{ states | count }}_{{ states("sensor.one") }}', hass)
+
+ refresh_runs = []
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ refresh_runs.append(updates.pop().result)
+
+ info = async_track_template_result(
+ hass,
+ [TrackTemplate(template_refresh, None, timedelta(seconds=5))],
+ refresh_listener,
+ )
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs == ["1_none"]
+ hass.states.async_set("sensor.one", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1_none", "1_any"]
+ info.async_refresh()
+ assert refresh_runs == ["1_none", "1_any"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1_none", "1_any"]
+ hass.states.async_set("sensor.three", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1_none", "1_any"]
+ hass.states.async_set("sensor.one", "none")
+ await hass.async_block_till_done()
+ assert refresh_runs == ["1_none", "1_any", "3_none"]
+
+
+async def test_track_two_templates_with_different_rate_limits(hass):
+ """Test two templates with different rate limits."""
+ template_one = Template("{{ states | count }} ", hass)
+ template_five = Template("{{ states | count }}", hass)
+
+ refresh_runs = {
+ template_one: [],
+ template_five: [],
+ }
+
+ @ha.callback
+ def refresh_listener(event, updates):
+ for update in updates:
+ refresh_runs[update.template].append(update.result)
+
+ info = async_track_template_result(
+ hass,
+ [
+ TrackTemplate(template_one, None, timedelta(seconds=0.1)),
+ TrackTemplate(template_five, None, timedelta(seconds=5)),
+ ],
+ refresh_listener,
+ )
+
+ await hass.async_block_till_done()
+ info.async_refresh()
+ await hass.async_block_till_done()
+
+ assert refresh_runs[template_one] == ["0"]
+ assert refresh_runs[template_five] == ["0"]
+ hass.states.async_set("sensor.one", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0"]
+ assert refresh_runs[template_five] == ["0"]
+ info.async_refresh()
+ assert refresh_runs[template_one] == ["0", "1"]
+ assert refresh_runs[template_five] == ["0", "1"]
+ hass.states.async_set("sensor.two", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0", "1"]
+ assert refresh_runs[template_five] == ["0", "1"]
+ next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 1)
+ with patch(
+ "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
+ ):
+ async_fire_time_changed(hass, next_time)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0", "1", "2"]
+ assert refresh_runs[template_five] == ["0", "1"]
+ hass.states.async_set("sensor.three", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0", "1", "2"]
+ assert refresh_runs[template_five] == ["0", "1"]
+ hass.states.async_set("sensor.four", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0", "1", "2"]
+ assert refresh_runs[template_five] == ["0", "1"]
+ hass.states.async_set("sensor.five", "any")
+ await hass.async_block_till_done()
+ assert refresh_runs[template_one] == ["0", "1", "2"]
+ assert refresh_runs[template_five] == ["0", "1"]
+
+
async def test_string(hass):
"""Test a string."""
template_refresh = Template("no_template", hass)
@@ -1412,7 +1817,7 @@ def refresh_listener(event, updates):
TrackTemplate(template_1, None),
TrackTemplate(template_2, None),
TrackTemplate(template_3, None),
- TrackTemplate(template_4, None),
+ TrackTemplate(template_4, None, timedelta(seconds=0)),
],
refresh_listener,
)
@@ -1460,6 +1865,25 @@ def refresh_listener(event, updates):
]
+async def test_async_track_template_result_raise_on_template_error(hass):
+ """Test that we raise as soon as we encounter a failed template."""
+
+ with pytest.raises(TemplateError):
+ async_track_template_result(
+ hass,
+ [
+ TrackTemplate(
+ Template(
+ "{{ states.switch | function_that_does_not_exist | list }}"
+ ),
+ None,
+ ),
+ ],
+ ha.callback(lambda event, updates: None),
+ raise_on_template_error=True,
+ )
+
+
async def test_track_same_state_simple_no_trigger(hass):
"""Test track_same_change with no trigger."""
callback_runs = []
diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py
index ed97b3e375778c..495c9d511bda7d 100644
--- a/tests/helpers/test_network.py
+++ b/tests/helpers/test_network.py
@@ -848,6 +848,14 @@ async def test_get_current_request_url_with_known_host(
== "http://homeassistant.local:8123"
)
+ with patch(
+ "homeassistant.helpers.network._get_request_host",
+ return_value="homeassistant",
+ ):
+ assert (
+ get_url(hass, require_current_request=True) == "http://homeassistant:8123"
+ )
+
with patch(
"homeassistant.helpers.network._get_request_host", return_value="unknown.local"
), pytest.raises(NoURLAvailableError):
diff --git a/tests/helpers/test_ratelimit.py b/tests/helpers/test_ratelimit.py
new file mode 100644
index 00000000000000..c34de9586dd894
--- /dev/null
+++ b/tests/helpers/test_ratelimit.py
@@ -0,0 +1,108 @@
+"""Tests for ratelimit."""
+import asyncio
+from datetime import timedelta
+
+from homeassistant.core import callback
+from homeassistant.helpers import ratelimit
+from homeassistant.util import dt as dt_util
+
+
+async def test_hit(hass):
+ """Test hitting the rate limit."""
+
+ refresh_called = False
+
+ @callback
+ def _refresh():
+ nonlocal refresh_called
+ refresh_called = True
+ return
+
+ rate_limiter = ratelimit.KeyedRateLimit(hass)
+ rate_limiter.async_triggered("key1", dt_util.utcnow())
+
+ assert (
+ rate_limiter.async_schedule_action(
+ "key1", timedelta(seconds=0.001), dt_util.utcnow(), _refresh
+ )
+ is not None
+ )
+
+ assert not refresh_called
+
+ assert rate_limiter.async_has_timer("key1")
+
+ await asyncio.sleep(0.002)
+ assert refresh_called
+
+ assert (
+ rate_limiter.async_schedule_action(
+ "key2", timedelta(seconds=0.001), dt_util.utcnow(), _refresh
+ )
+ is None
+ )
+ rate_limiter.async_remove()
+
+
+async def test_miss(hass):
+ """Test missing the rate limit."""
+
+ refresh_called = False
+
+ @callback
+ def _refresh():
+ nonlocal refresh_called
+ refresh_called = True
+ return
+
+ rate_limiter = ratelimit.KeyedRateLimit(hass)
+ assert (
+ rate_limiter.async_schedule_action(
+ "key1", timedelta(seconds=0.1), dt_util.utcnow(), _refresh
+ )
+ is None
+ )
+ assert not refresh_called
+ assert not rate_limiter.async_has_timer("key1")
+
+ assert (
+ rate_limiter.async_schedule_action(
+ "key1", timedelta(seconds=0.1), dt_util.utcnow(), _refresh
+ )
+ is None
+ )
+ assert not refresh_called
+ assert not rate_limiter.async_has_timer("key1")
+ rate_limiter.async_remove()
+
+
+async def test_no_limit(hass):
+ """Test async_schedule_action always return None when there is no rate limit."""
+
+ refresh_called = False
+
+ @callback
+ def _refresh():
+ nonlocal refresh_called
+ refresh_called = True
+ return
+
+ rate_limiter = ratelimit.KeyedRateLimit(hass)
+ rate_limiter.async_triggered("key1", dt_util.utcnow())
+
+ assert (
+ rate_limiter.async_schedule_action("key1", None, dt_util.utcnow(), _refresh)
+ is None
+ )
+ assert not refresh_called
+ assert not rate_limiter.async_has_timer("key1")
+
+ rate_limiter.async_triggered("key1", dt_util.utcnow())
+
+ assert (
+ rate_limiter.async_schedule_action("key1", None, dt_util.utcnow(), _refresh)
+ is None
+ )
+ assert not refresh_called
+ assert not rate_limiter.async_has_timer("key1")
+ rate_limiter.async_remove()
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 0bd353e1fa0d5f..93bb249c4855a5 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -16,6 +16,7 @@
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
from homeassistant.core import Context, CoreState, callback
from homeassistant.helpers import config_validation as cv, script
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.async_mock import patch
@@ -1332,6 +1333,10 @@ async def test_referenced_entities(hass):
"service": "test.script",
"data": {"entity_id": ["light.service_list"]},
},
+ {
+ "service": "test.script",
+ "data": {"entity_id": "{{ 'light.service_template' }}"},
+ },
{
"condition": "state",
"entity_id": "sensor.condition",
@@ -1824,3 +1829,114 @@ async def test_set_redefines_variable(hass, caplog):
assert mock_calls[0].data["value"] == "1"
assert mock_calls[1].data["value"] == "2"
+
+
+async def test_validate_action_config(hass):
+ """Validate action config."""
+ configs = {
+ cv.SCRIPT_ACTION_CALL_SERVICE: {"service": "light.turn_on"},
+ cv.SCRIPT_ACTION_DELAY: {"delay": 5},
+ cv.SCRIPT_ACTION_WAIT_TEMPLATE: {
+ "wait_template": "{{ states.light.kitchen.state == 'on' }}"
+ },
+ cv.SCRIPT_ACTION_FIRE_EVENT: {"event": "my_event"},
+ cv.SCRIPT_ACTION_CHECK_CONDITION: {
+ "condition": "{{ states.light.kitchen.state == 'on' }}"
+ },
+ cv.SCRIPT_ACTION_DEVICE_AUTOMATION: {
+ "domain": "light",
+ "entity_id": "light.kitchen",
+ "device_id": "abcd",
+ "type": "turn_on",
+ },
+ cv.SCRIPT_ACTION_ACTIVATE_SCENE: {"scene": "scene.relax"},
+ cv.SCRIPT_ACTION_REPEAT: {
+ "repeat": {"count": 3, "sequence": [{"event": "repeat_event"}]}
+ },
+ cv.SCRIPT_ACTION_CHOOSE: {
+ "choose": [
+ {
+ "condition": "{{ states.light.kitchen.state == 'on' }}",
+ "sequence": [{"event": "choose_event"}],
+ }
+ ],
+ "default": [{"event": "choose_default_event"}],
+ },
+ cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: {
+ "wait_for_trigger": [
+ {"platform": "event", "event_type": "wait_for_trigger_event"}
+ ]
+ },
+ cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}},
+ }
+
+ for key in cv.ACTION_TYPE_SCHEMAS:
+ assert key in configs, f"No validate config test found for {key}"
+
+ # Verify we raise if we don't know the action type
+ with patch(
+ "homeassistant.helpers.config_validation.determine_script_action",
+ return_value="non-existing",
+ ), pytest.raises(ValueError):
+ await script.async_validate_action_config(hass, {})
+
+ for action_type, config in configs.items():
+ assert cv.determine_script_action(config) == action_type
+ try:
+ await script.async_validate_action_config(hass, config)
+ except vol.Invalid as err:
+ assert False, f"{action_type} config invalid: {err}"
+
+
+async def test_embedded_wait_for_trigger_in_automation(hass):
+ """Test an embedded wait for trigger."""
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": {
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {
+ "repeat": {
+ "while": [
+ {
+ "condition": "template",
+ "value_template": '{{ is_state("test.value1", "trigger-while") }}',
+ }
+ ],
+ "sequence": [
+ {"event": "trigger_wait_event"},
+ {
+ "wait_for_trigger": [
+ {
+ "platform": "template",
+ "value_template": '{{ is_state("test.value2", "trigger-wait") }}',
+ }
+ ]
+ },
+ {"service": "test.script"},
+ ],
+ }
+ },
+ }
+ },
+ )
+
+ hass.states.async_set("test.value1", "trigger-while")
+ hass.states.async_set("test.value2", "not-trigger-wait")
+ mock_calls = async_mock_service(hass, "test", "script")
+
+ async def trigger_wait_event(_):
+ # give script the time to attach the trigger.
+ await asyncio.sleep(0)
+ hass.states.async_set("test.value1", "not-trigger-while")
+ hass.states.async_set("test.value2", "trigger-wait")
+
+ hass.bus.async_listen("trigger_wait_event", trigger_wait_event)
+
+ # Start automation
+ hass.bus.async_fire("test_event")
+
+ await hass.async_block_till_done()
+
+ assert len(mock_calls) == 1
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index 1a0cdb79bbc59b..929df2a32e09b9 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -267,6 +267,8 @@ async def test_extract_entity_ids(hass):
hass.states.async_set("light.Ceiling", STATE_OFF)
hass.states.async_set("light.Kitchen", STATE_OFF)
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await hass.components.group.Group.async_create_group(
hass, "test", ["light.Ceiling", "light.Kitchen"]
)
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 81deb46f928505..5535fa53993429 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -8,6 +8,7 @@
from homeassistant.components import group
from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
LENGTH_METERS,
MASS_GRAMS,
MATCH_ALL,
@@ -17,6 +18,7 @@
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import UnitSystem
@@ -53,16 +55,17 @@ def assert_result_info(info, result, entities=None, domains=None, all_states=Fal
"""Check result info."""
assert info.result() == result
assert info.all_states == all_states
- assert info.filter_lifecycle("invalid_entity_name.somewhere") == all_states
+ assert info.filter("invalid_entity_name.somewhere") == all_states
if entities is not None:
assert info.entities == frozenset(entities)
assert all([info.filter(entity) for entity in entities])
- assert not info.filter("invalid_entity_name.somewhere")
+ if not all_states:
+ assert not info.filter("invalid_entity_name.somewhere")
else:
assert not info.entities
if domains is not None:
assert info.domains == frozenset(domains)
- assert all([info.filter_lifecycle(domain + ".entity") for domain in domains])
+ assert all([info.filter(domain + ".entity") for domain in domains])
else:
assert not hasattr(info, "_domains")
@@ -146,14 +149,31 @@ def test_iterating_all_states(hass):
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "", all_states=True)
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
hass.states.async_set("test.object", "happy")
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
- assert_result_info(
- info, "10happy", entities=["test.object", "sensor.temperature"], all_states=True
- )
+ assert_result_info(info, "10happy", entities=[], all_states=True)
+
+
+def test_iterating_all_states_unavailable(hass):
+ """Test iterating all states unavailable."""
+ hass.states.async_set("test.object", "on")
+
+ tmpl_str = "{{ states | selectattr('state', 'in', ['unavailable', 'unknown', 'none']) | list | count }}"
+
+ info = render_to_info(hass, tmpl_str)
+
+ assert info.all_states is True
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
+
+ hass.states.async_set("test.object", "unknown")
+ hass.states.async_set("sensor.temperature", 10)
+
+ info = render_to_info(hass, tmpl_str)
+ assert_result_info(info, "1", entities=[], all_states=True)
def test_iterating_domain_states(hass):
@@ -162,6 +182,7 @@ def test_iterating_domain_states(hass):
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "", domains=["sensor"])
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
hass.states.async_set("test.object", "happy")
hass.states.async_set("sensor.back_door", "open")
@@ -171,7 +192,7 @@ def test_iterating_domain_states(hass):
assert_result_info(
info,
"open10",
- entities=["sensor.back_door", "sensor.temperature"],
+ entities=[],
domains=["sensor"],
)
@@ -1331,12 +1352,15 @@ async def test_closest_function_home_vs_group_entity_id(hass):
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(hass, "location group", ["test_domain.object"])
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
assert_result_info(
info, "test_domain.object", {"group.location_group", "test_domain.object"}
)
+ assert info.rate_limit is None
async def test_closest_function_home_vs_group_state(hass):
@@ -1356,26 +1380,32 @@ async def test_closest_function_home_vs_group_state(hass):
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(hass, "location group", ["test_domain.object"])
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
assert_result_info(
info, "test_domain.object", {"group.location_group", "test_domain.object"}
)
+ assert info.rate_limit is None
info = render_to_info(hass, "{{ closest(states.group.location_group).entity_id }}")
assert_result_info(
info, "test_domain.object", {"test_domain.object", "group.location_group"}
)
+ assert info.rate_limit is None
async def test_expand(hass):
"""Test expand function."""
info = render_to_info(hass, "{{ expand('test.object') }}")
assert_result_info(info, "[]", ["test.object"])
+ assert info.rate_limit is None
info = render_to_info(hass, "{{ expand(56) }}")
assert_result_info(info, "[]")
+ assert info.rate_limit is None
hass.states.async_set("test.object", "happy")
@@ -1383,18 +1413,23 @@ async def test_expand(hass):
hass, "{{ expand('test.object') | map(attribute='entity_id') | join(', ') }}"
)
assert_result_info(info, "test.object", ["test.object"])
+ assert info.rate_limit is None
info = render_to_info(
hass,
"{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}",
)
assert_result_info(info, "", ["group.new_group"])
+ assert info.rate_limit is None
info = render_to_info(
hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}"
)
assert_result_info(info, "", [], ["group"])
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(hass, "new group", ["test.object"])
info = render_to_info(
@@ -1402,13 +1437,13 @@ async def test_expand(hass):
"{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}",
)
assert_result_info(info, "test.object", {"group.new_group", "test.object"})
+ assert info.rate_limit is None
info = render_to_info(
hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}"
)
- assert_result_info(
- info, "test.object", {"test.object", "group.new_group"}, ["group"]
- )
+ assert_result_info(info, "test.object", {"test.object"}, ["group"])
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
info = render_to_info(
hass,
@@ -1423,10 +1458,14 @@ async def test_expand(hass):
" | map(attribute='entity_id') | join(', ') }}",
)
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
+ assert info.rate_limit is None
hass.states.async_set("sensor.power_1", 0)
hass.states.async_set("sensor.power_2", 200.2)
hass.states.async_set("sensor.power_3", 400.4)
+
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(
hass, "power sensors", ["sensor.power_1", "sensor.power_2", "sensor.power_3"]
)
@@ -1440,6 +1479,7 @@ async def test_expand(hass):
str(200.2 + 400.4),
{"group.power_sensors", "sensor.power_1", "sensor.power_2", "sensor.power_3"},
)
+ assert info.rate_limit is None
def test_closest_function_to_coord(hass):
@@ -1505,6 +1545,7 @@ def test_async_render_to_info_with_branching(hass):
""",
)
assert_result_info(info, "off", {"light.a", "light.c"})
+ assert info.rate_limit is None
info = render_to_info(
hass,
@@ -1516,6 +1557,7 @@ def test_async_render_to_info_with_branching(hass):
""",
)
assert_result_info(info, "on", {"light.a", "light.b"})
+ assert info.rate_limit is None
def test_async_render_to_info_with_complex_branching(hass):
@@ -1552,13 +1594,14 @@ def test_async_render_to_info_with_complex_branching(hass):
)
assert_result_info(info, "['sensor.a']", {"light.a", "light.b"}, {"sensor"})
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
async def test_async_render_to_info_with_wildcard_matching_entity_id(hass):
"""Test tracking template with a wildcard."""
template_complex_str = r"""
-{% for state in states %}
+{% for state in states.cover %}
{% if state.entity_id | regex_match('.*\.office_') %}
{{ state.entity_id }}={{ state.state }}
{% endif %}
@@ -1570,13 +1613,10 @@ async def test_async_render_to_info_with_wildcard_matching_entity_id(hass):
hass.states.async_set("cover.office_skylight", "open")
info = render_to_info(hass, template_complex_str)
- assert not info.domains
- assert info.entities == {
- "cover.office_drapes",
- "cover.office_window",
- "cover.office_skylight",
- }
- assert info.all_states is True
+ assert info.domains == {"cover"}
+ assert info.entities == set()
+ assert info.all_states is False
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
async def test_async_render_to_info_with_wildcard_matching_state(hass):
@@ -1599,27 +1639,17 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
info = render_to_info(hass, template_complex_str)
assert not info.domains
- assert info.entities == {
- "cover.x_skylight",
- "binary_sensor.door",
- "cover.office_drapes",
- "cover.office_window",
- "cover.office_skylight",
- }
+ assert info.entities == set()
assert info.all_states is True
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
hass.states.async_set("binary_sensor.door", "closed")
info = render_to_info(hass, template_complex_str)
assert not info.domains
- assert info.entities == {
- "cover.x_skylight",
- "binary_sensor.door",
- "cover.office_drapes",
- "cover.office_window",
- "cover.office_skylight",
- }
+ assert info.entities == set()
assert info.all_states is True
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
template_cover_str = """
@@ -1634,13 +1664,9 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
info = render_to_info(hass, template_cover_str)
assert info.domains == {"cover"}
- assert info.entities == {
- "cover.x_skylight",
- "cover.office_drapes",
- "cover.office_window",
- "cover.office_skylight",
- }
+ assert info.entities == set()
assert info.all_states is False
+ assert info.rate_limit == template.DEFAULT_RATE_LIMIT
def test_nested_async_render_to_info_case(hass):
@@ -1653,6 +1679,7 @@ def test_nested_async_render_to_info_case(hass):
hass, "{{ states[states['input_select.picker'].state].state }}", {}
)
assert_result_info(info, "off", {"input_select.picker", "vacuum.a"})
+ assert info.rate_limit is None
def test_result_as_boolean(hass):
@@ -1931,9 +1958,7 @@ def test_generate_filter_iterators(hass):
{% endfor %}
""",
)
- assert_result_info(
- info, "sensor.test_sensor=off,", ["sensor.test_sensor"], ["sensor"]
- )
+ assert_result_info(info, "sensor.test_sensor=off,", [], ["sensor"])
info = render_to_info(
hass,
@@ -1943,9 +1968,7 @@ def test_generate_filter_iterators(hass):
{% endfor %}
""",
)
- assert_result_info(
- info, "sensor.test_sensor=value,", ["sensor.test_sensor"], ["sensor"]
- )
+ assert_result_info(info, "sensor.test_sensor=value,", [], ["sensor"])
def test_generate_select(hass):
@@ -1957,7 +1980,8 @@ def test_generate_select(hass):
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
- assert_result_info(info, "", [], ["sensor"])
+ assert_result_info(info, "", [], [])
+ assert info.domains_lifecycle == {"sensor"}
hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"})
hass.states.async_set("sensor.test_sensor_on", "on")
@@ -1966,9 +1990,10 @@ def test_generate_select(hass):
assert_result_info(
info,
"sensor.test_sensor",
- ["sensor.test_sensor", "sensor.test_sensor_on"],
+ [],
["sensor"],
)
+ assert info.domains_lifecycle == {"sensor"}
async def test_async_render_to_info_in_conditional(hass):
@@ -2091,6 +2116,8 @@ async def test_extract_entities_match_entities(hass, allow_extract_entities):
)
)
+ assert await async_setup_component(hass, "group", {})
+ await hass.async_block_till_done()
await group.Group.async_create_group(hass, "empty group", [])
assert ["group.empty_group"] == template.extract_entities(
@@ -2271,7 +2298,7 @@ def test_jinja_namespace(hass):
def test_state_with_unit(hass):
"""Test the state_with_unit property helper."""
- hass.states.async_set("sensor.test", "23", {"unit_of_measurement": "beers"})
+ hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "wow")
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
@@ -2419,3 +2446,165 @@ async def test_demo_template(hass):
assert "sensor0" in result
assert "sensor1" in result
assert "sun" in result
+
+
+async def test_slice_states(hass):
+ """Test iterating states with a slice."""
+ hass.states.async_set("sensor.test", "23")
+
+ tpl = template.Template(
+ "{% for states in states | slice(1) -%}{% set state = states | first %}{{ state.entity_id }}{%- endfor %}",
+ hass,
+ )
+ assert tpl.async_render() == "sensor.test"
+
+
+async def test_lifecycle(hass):
+ """Test that we limit template render info for lifecycle events."""
+ hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"})
+ for i in range(2):
+ hass.states.async_set(f"sensor.sensor{i}", "on")
+
+ tmp = template.Template("{{ states | count }}", hass)
+
+ info = tmp.async_render_to_info()
+ assert info.all_states is False
+ assert info.all_states_lifecycle is True
+ assert info.rate_limit is None
+ assert info.entities == set()
+ assert info.domains == set()
+ assert info.domains_lifecycle == set()
+
+ assert info.filter("sun.sun") is False
+ assert info.filter("sensor.sensor1") is False
+ assert info.filter_lifecycle("sensor.new") is True
+ assert info.filter_lifecycle("sensor.removed") is True
+
+
+async def test_template_timeout(hass):
+ """Test to see if a template will timeout."""
+ for i in range(2):
+ hass.states.async_set(f"sensor.sensor{i}", "on")
+
+ tmp = template.Template("{{ states | count }}", hass)
+ assert await tmp.async_render_will_timeout(3) is False
+
+ tmp2 = template.Template("{{ error_invalid + 1 }}", hass)
+ assert await tmp2.async_render_will_timeout(3) is False
+
+ tmp3 = template.Template("static", hass)
+ assert await tmp3.async_render_will_timeout(3) is False
+
+ tmp4 = template.Template("{{ var1 }}", hass)
+ assert await tmp4.async_render_will_timeout(3, {"var1": "ok"}) is False
+
+ slow_template_str = """
+{% for var in range(1000) -%}
+ {% for var in range(1000) -%}
+ {{ var }}
+ {%- endfor %}
+{%- endfor %}
+"""
+ tmp5 = template.Template(slow_template_str, hass)
+ assert await tmp5.async_render_will_timeout(0.000001) is True
+
+
+async def test_lights(hass):
+ """Test we can sort lights."""
+
+ tmpl = """
+ {% set lights_on = states.light|selectattr('state','eq','on')|map(attribute='name')|list %}
+ {% if lights_on|length == 0 %}
+ No lights on. Sleep well..
+ {% elif lights_on|length == 1 %}
+ The {{lights_on[0]}} light is on.
+ {% elif lights_on|length == 2 %}
+ The {{lights_on[0]}} and {{lights_on[1]}} lights are on.
+ {% else %}
+ The {{lights_on[:-1]|join(', ')}}, and {{lights_on[-1]}} lights are on.
+ {% endif %}
+ """
+ states = []
+ for i in range(10):
+ states.append(f"light.sensor{i}")
+ hass.states.async_set(f"light.sensor{i}", "on")
+
+ tmp = template.Template(tmpl, hass)
+ info = tmp.async_render_to_info()
+ assert info.entities == set()
+ assert info.domains == {"light"}
+
+ assert "lights are on" in info.result()
+ for i in range(10):
+ assert f"sensor{i}" in info.result()
+
+
+async def test_state_attributes(hass):
+ """Test state attributes."""
+ hass.states.async_set("sensor.test", "23")
+
+ tpl = template.Template(
+ "{{ states.sensor.test.last_changed }}",
+ hass,
+ )
+ assert tpl.async_render() == str(hass.states.get("sensor.test").last_changed)
+
+ tpl = template.Template(
+ "{{ states.sensor.test.object_id }}",
+ hass,
+ )
+ assert tpl.async_render() == hass.states.get("sensor.test").object_id
+
+ tpl = template.Template(
+ "{{ states.sensor.test.domain }}",
+ hass,
+ )
+ assert tpl.async_render() == hass.states.get("sensor.test").domain
+
+ tpl = template.Template(
+ "{{ states.sensor.test.context.id }}",
+ hass,
+ )
+ assert tpl.async_render() == hass.states.get("sensor.test").context.id
+
+ tpl = template.Template(
+ "{{ states.sensor.test.state_with_unit }}",
+ hass,
+ )
+ assert tpl.async_render() == "23"
+
+ tpl = template.Template(
+ "{{ states.sensor.test.invalid_prop }}",
+ hass,
+ )
+ assert tpl.async_render() == ""
+
+ tpl = template.Template(
+ "{{ states.sensor.test.invalid_prop.xx }}",
+ hass,
+ )
+ with pytest.raises(TemplateError):
+ tpl.async_render()
+
+
+async def test_unavailable_states(hass):
+ """Test watching unavailable states."""
+
+ for i in range(10):
+ hass.states.async_set(f"light.sensor{i}", "on")
+
+ hass.states.async_set("light.unavailable", "unavailable")
+ hass.states.async_set("light.unknown", "unknown")
+ hass.states.async_set("light.none", "none")
+
+ tpl = template.Template(
+ "{{ states | selectattr('state', 'in', ['unavailable','unknown','none']) | map(attribute='entity_id') | list | join(', ') }}",
+ hass,
+ )
+ assert tpl.async_render() == "light.none, light.unavailable, light.unknown"
+
+ tpl = template.Template(
+ "{{ states.light | selectattr('state', 'in', ['unavailable','unknown','none']) | map(attribute='entity_id') | list | join(', ') }}",
+ hass,
+ )
+ assert tpl.async_render() == "light.none, light.unavailable, light.unknown"
diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py
index 1049108f9de62a..41c0aa5727ba0b 100644
--- a/tests/mock/zwave.py
+++ b/tests/mock/zwave.py
@@ -106,7 +106,7 @@ class MockNode(MagicMock):
def __init__(
self,
*,
- node_id="567",
+ node_id=567,
name="Mock Node",
manufacturer_id="ABCD",
product_id="123",
diff --git a/tests/test_config.py b/tests/test_config.py
index c5443666bf5315..181e80da30c7e4 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -369,6 +369,36 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
assert hass.config.config_source == SOURCE_STORAGE
+async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_storage):
+ """Test loading core and YAML config onto hass object."""
+ hass_storage["core.config"] = {
+ "data": {
+ "elevation": 10,
+ "latitude": 55,
+ "location_name": "Home",
+ "longitude": 13,
+ "time_zone": "Europe/Copenhagen",
+ "unit_system": "metric",
+ },
+ "key": "core.config",
+ "version": 1,
+ }
+ await config_util.async_process_ha_core_config(
+ hass, {"media_dirs": {"mymedia": "/usr"}, "allowlist_external_dirs": "/etc"}
+ )
+
+ assert hass.config.latitude == 55
+ assert hass.config.longitude == 13
+ assert hass.config.elevation == 10
+ assert hass.config.location_name == "Home"
+ assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
+ assert hass.config.time_zone.zone == "Europe/Copenhagen"
+ assert len(hass.config.allowlist_external_dirs) == 3
+ assert "/etc" in hass.config.allowlist_external_dirs
+ assert hass.config.media_dirs == {"mymedia": "/usr"}
+ assert hass.config.config_source == SOURCE_STORAGE
+
+
async def test_updating_configuration(hass, hass_storage):
"""Test updating configuration stores the new configuration."""
core_data = {
@@ -440,6 +470,7 @@ async def test_loading_configuration(hass):
"allowlist_external_dirs": "/etc",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
+ "media_dirs": {"mymedia": "/usr"},
},
)
@@ -453,6 +484,8 @@ async def test_loading_configuration(hass):
assert hass.config.internal_url == "http://example.local"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
+ assert "/usr" in hass.config.allowlist_external_dirs
+ assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source == config_util.SOURCE_YAML
@@ -483,6 +516,22 @@ async def test_loading_configuration_temperature_unit(hass):
assert hass.config.config_source == config_util.SOURCE_YAML
+async def test_loading_configuration_default_media_dirs_docker(hass):
+ """Test loading core config onto hass object."""
+ with patch("homeassistant.config.is_docker_env", return_value=True):
+ await config_util.async_process_ha_core_config(
+ hass,
+ {
+ "name": "Huis",
+ },
+ )
+
+ assert hass.config.location_name == "Huis"
+ assert len(hass.config.allowlist_external_dirs) == 2
+ assert "/media" in hass.config.allowlist_external_dirs
+ assert hass.config.media_dirs == {"local": "/media"}
+
+
async def test_loading_configuration_from_packages(hass):
"""Test loading packages config onto hass object config."""
await config_util.async_process_ha_core_config(
diff --git a/tests/test_core.py b/tests/test_core.py
index f5de9c5f1a1f4e..1ae1f32a10a8d6 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -268,49 +268,77 @@ async def test_add_job_with_none(hass):
hass.async_add_job(None, "test_arg")
-class TestEvent(unittest.TestCase):
- """A Test Event class."""
-
- def test_eq(self):
- """Test events."""
- now = dt_util.utcnow()
- data = {"some": "attr"}
- context = ha.Context()
- event1, event2 = [
- ha.Event("some_type", data, time_fired=now, context=context)
- for _ in range(2)
- ]
+def test_event_eq():
+ """Test events."""
+ now = dt_util.utcnow()
+ data = {"some": "attr"}
+ context = ha.Context()
+ event1, event2 = [
+ ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2)
+ ]
- assert event1 == event2
+ assert event1 == event2
- def test_repr(self):
- """Test that repr method works."""
- assert str(ha.Event("TestEvent")) == ""
- assert (
- str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote))
- == ""
- )
+def test_event_repr():
+ """Test that Event repr method works."""
+ assert str(ha.Event("TestEvent")) == ""
- def test_as_dict(self):
- """Test as dictionary."""
- event_type = "some_type"
- now = dt_util.utcnow()
- data = {"some": "attr"}
+ assert (
+ str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote))
+ == ""
+ )
- event = ha.Event(event_type, data, ha.EventOrigin.local, now)
- expected = {
- "event_type": event_type,
- "data": data,
- "origin": "LOCAL",
- "time_fired": now,
- "context": {
- "id": event.context.id,
- "parent_id": None,
- "user_id": event.context.user_id,
- },
- }
- assert expected == event.as_dict()
+
+def test_event_as_dict():
+ """Test an Event as dictionary."""
+ event_type = "some_type"
+ now = dt_util.utcnow()
+ data = {"some": "attr"}
+
+ event = ha.Event(event_type, data, ha.EventOrigin.local, now)
+ expected = {
+ "event_type": event_type,
+ "data": data,
+ "origin": "LOCAL",
+ "time_fired": now.isoformat(),
+ "context": {
+ "id": event.context.id,
+ "parent_id": None,
+ "user_id": event.context.user_id,
+ },
+ }
+ assert event.as_dict() == expected
+ # 2nd time to verify cache
+ assert event.as_dict() == expected
+
+
+def test_state_as_dict():
+ """Test a State as dictionary."""
+ last_time = datetime(1984, 12, 8, 12, 0, 0)
+ state = ha.State(
+ "happy.happy",
+ "on",
+ {"pig": "dog"},
+ last_updated=last_time,
+ last_changed=last_time,
+ )
+ expected = {
+ "context": {
+ "id": state.context.id,
+ "parent_id": None,
+ "user_id": state.context.user_id,
+ },
+ "entity_id": "happy.happy",
+ "attributes": {"pig": "dog"},
+ "last_changed": last_time.isoformat(),
+ "last_updated": last_time.isoformat(),
+ "state": "on",
+ }
+ assert state.as_dict() == expected
+ # 2nd time to verify cache
+ assert state.as_dict() == expected
+ assert state.as_dict() is state.as_dict()
class TestEventBus(unittest.TestCase):
@@ -1477,3 +1505,20 @@ async def test_async_all(hass):
assert {
state.entity_id for state in hass.states.async_all(["light", "switch"])
} == {"light.bowl", "light.frog", "switch.link"}
+
+
+async def test_async_entity_ids_count(hass):
+ """Test async_entity_ids_count."""
+
+ hass.states.async_set("switch.link", "on")
+ hass.states.async_set("light.bowl", "on")
+ hass.states.async_set("light.frog", "on")
+ hass.states.async_set("vacuum.floor", "on")
+
+ assert hass.states.async_entity_ids_count() == 4
+ assert hass.states.async_entity_ids_count("light") == 2
+
+ hass.states.async_set("light.cow", "on")
+
+ assert hass.states.async_entity_ids_count() == 5
+ assert hass.states.async_entity_ids_count("light") == 3
diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py
index 8d0cdf6ac9369a..d467a93fd6214d 100644
--- a/tests/testing_config/custom_components/test/sensor.py
+++ b/tests/testing_config/custom_components/test/sensor.py
@@ -4,7 +4,7 @@
Call init before using it in your tests to ensure clean test data.
"""
import homeassistant.components.sensor as sensor
-from homeassistant.const import PERCENTAGE
+from homeassistant.const import PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS
from tests.common import MockEntity
@@ -15,14 +15,14 @@
sensor.DEVICE_CLASS_BATTERY: PERCENTAGE, # % of battery that is left
sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air
sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm)
- sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm)
+ sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm)
sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F)
sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601)
- sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar)
+ sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar)
sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW)
sensor.DEVICE_CLASS_CURRENT: "A", # current (A)
sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh)
- sensor.DEVICE_CLASS_POWER_FACTOR: "%", # power factor (no unit, min: -1.0, max: 1.0)
+ sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0)
sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V)
}
diff --git a/tests/util/test_thread.py b/tests/util/test_thread.py
new file mode 100644
index 00000000000000..d5f05f5c93ee10
--- /dev/null
+++ b/tests/util/test_thread.py
@@ -0,0 +1,55 @@
+"""Test Home Assistant thread utils."""
+
+import asyncio
+
+import pytest
+
+from homeassistant.util.async_ import run_callback_threadsafe
+from homeassistant.util.thread import ThreadWithException
+
+
+async def test_thread_with_exception_invalid(hass):
+ """Test throwing an invalid thread exception."""
+
+ finish_event = asyncio.Event()
+
+ def _do_nothing(*_):
+ run_callback_threadsafe(hass.loop, finish_event.set)
+
+ test_thread = ThreadWithException(target=_do_nothing)
+ test_thread.start()
+ await asyncio.wait_for(finish_event.wait(), timeout=0.1)
+
+ with pytest.raises(TypeError):
+ test_thread.raise_exc(_EmptyClass())
+ test_thread.join()
+
+
+async def test_thread_not_started(hass):
+ """Test throwing when the thread is not started."""
+
+ test_thread = ThreadWithException(target=lambda *_: None)
+
+ with pytest.raises(AssertionError):
+ test_thread.raise_exc(TimeoutError)
+
+
+async def test_thread_fails_raise(hass):
+ """Test throwing after already ended."""
+
+ finish_event = asyncio.Event()
+
+ def _do_nothing(*_):
+ run_callback_threadsafe(hass.loop, finish_event.set)
+
+ test_thread = ThreadWithException(target=_do_nothing)
+ test_thread.start()
+ await asyncio.wait_for(finish_event.wait(), timeout=0.1)
+ test_thread.join()
+
+ with pytest.raises(SystemError):
+ test_thread.raise_exc(ValueError)
+
+
+class _EmptyClass:
+ """An empty class."""
diff --git a/tox.ini b/tox.ini
index 7829a7a98d7954..cc1df307bfe42d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
[tox]
-envlist = py36, py37, py38, lint, pylint, typing, cov
+envlist = py37, py38, lint, pylint, typing, cov
skip_missing_interpreters = True
+ignore_basepython_conflict = True
[testenv]
basepython = {env:PYTHON3_PATH:python3}