diff --git a/.coveragerc b/.coveragerc
index 162e0c65f06..03929d4ce39 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
@@ -803,6 +818,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
@@ -830,7 +846,9 @@ omit =
homeassistant/components/synology_chat/notify.py
homeassistant/components/synology_dsm/__init__.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
@@ -844,7 +862,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
@@ -863,7 +887,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
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fae621670ec..3d65df477e7 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -518,25 +518,24 @@ jobs:
hassfest:
name: Check hassfest
runs-on: ubuntu-latest
- needs: prepare-base
+ needs: prepare-tests
+ strategy:
+ matrix:
+ python-version: [3.7]
+ container: homeassistant/ci-azure:${{ matrix.python-version }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v2
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v2.1.2
- id: python
- with:
- python-version: ${{ env.DEFAULT_PYTHON }}
- - name: Restore base Python virtual environment
+ - name:
+ Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache@v2
with:
path: venv
key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
+ ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
+ matrix.python-version }}-${{ hashFiles('requirements_test.txt')
+ }}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -546,7 +545,7 @@ jobs:
- name: Run hassfest
run: |
. venv/bin/activate
- python -m script.hassfest --action validate
+ python -m script.hassfest --requirements --action validate
gen-requirements-all:
name: Check all requirements
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 91add974b8d..114e6b6c87b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -39,7 +39,7 @@ repos:
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
- rev: 5.5.1
+ rev: 5.5.3
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/CODEOWNERS b/CODEOWNERS
index 75714f1aede..31a7bc6356b 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
@@ -349,6 +353,7 @@ 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/ring/* @balloob
@@ -357,6 +362,7 @@ 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 +405,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
@@ -421,7 +429,7 @@ homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron
-homeassistant/components/synology_dsm/* @ProtoThis @Quentame
+homeassistant/components/synology_dsm/* @hacf-fr @Quentame
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
homeassistant/components/tado/* @michaelarnauts @bdraco
@@ -502,6 +510,7 @@ 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/zwave/* @home-assistant/z-wave
diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py
index e572feafcf8..5696a35ea2f 100644
--- a/homeassistant/components/accuweather/const.py
+++ b/homeassistant/components/accuweather/const.py
@@ -5,6 +5,7 @@ from homeassistant.const import (
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_METERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
@@ -24,7 +25,6 @@ ATTR_UNIT_METRIC = "Metric"
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 @@ SENSOR_TYPES = {
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/translations/de.json b/homeassistant/components/accuweather/translations/de.json
new file mode 100644
index 00000000000..320c834e920
--- /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 00000000000..51cf2050ba7
--- /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 e5ed21357e0..40cf1ccc0b9 100644
--- a/homeassistant/components/accuweather/translations/fr.json
+++ b/homeassistant/components/accuweather/translations/fr.json
@@ -3,9 +3,32 @@
"abort": {
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "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/no.json b/homeassistant/components/accuweather/translations/no.json
index f0ab46267b2..c6cbc82bc2c 100644
--- a/homeassistant/components/accuweather/translations/no.json
+++ b/homeassistant/components/accuweather/translations/no.json
@@ -16,7 +16,8 @@
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjon"
},
- "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene."
+ "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.",
+ "title": ""
}
}
},
diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json
index a518c287b11..052ed5b6236 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 00000000000..ca58cd9ab6b
--- /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/sensor.fr.json b/homeassistant/components/accuweather/translations/sensor.fr.json
new file mode 100644
index 00000000000..cd0a04eceee
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.fr.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "En baisse",
+ "rising": "En hausse",
+ "steady": "Stable"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json
index 5364fc683eb..66335077cfb 100644
--- a/homeassistant/components/acmeda/translations/no.json
+++ b/homeassistant/components/acmeda/translations/no.json
@@ -11,5 +11,6 @@
"title": "Velg en hub du vil legge til"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json
index 725ca4a7a32..a772988c042 100644
--- a/homeassistant/components/adguard/translations/no.json
+++ b/homeassistant/components/adguard/translations/no.json
@@ -16,6 +16,7 @@
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
"username": "Brukernavn",
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json
index ed034fcd1db..2e9ce8c0b3c 100644
--- a/homeassistant/components/adguard/translations/pl.json
+++ b/homeassistant/components/adguard/translations/pl.json
@@ -5,7 +5,7 @@
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
},
"error": {
- "connection_error": "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/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py
index df8a74dc1d5..d481420b4d4 100644
--- a/homeassistant/components/ads/binary_sensor.py
+++ b/homeassistant/components/ads/binary_sensor.py
@@ -4,6 +4,7 @@ import logging
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/translations/no.json b/homeassistant/components/agent_dvr/translations/no.json
index 247654fdde9..3fcbb8f1617 100644
--- a/homeassistant/components/agent_dvr/translations/no.json
+++ b/homeassistant/components/agent_dvr/translations/no.json
@@ -10,7 +10,8 @@
"step": {
"user": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
"title": "Konfigurere Agent DVR"
}
diff --git a/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant/components/agent_dvr/translations/pl.json
index 5045015087f..9c101555f78 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/air_quality/group.py b/homeassistant/components/air_quality/group.py
new file mode 100644
index 00000000000..4741f8a3b54
--- /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/translations/et.json b/homeassistant/components/airly/translations/et.json
new file mode 100644
index 00000000000..aae3ef835bb
--- /dev/null
+++ b/homeassistant/components/airly/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/airly/translations/no.json b/homeassistant/components/airly/translations/no.json
index 6222dddfe28..09e77a311eb 100644
--- a/homeassistant/components/airly/translations/no.json
+++ b/homeassistant/components/airly/translations/no.json
@@ -15,7 +15,8 @@
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjonen"
},
- "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)"
+ "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json
index c7081cd694a..ae35beabf6b 100644
--- a/homeassistant/components/airly/translations/pt.json
+++ b/homeassistant/components/airly/translations/pt.json
@@ -3,9 +3,11 @@
"step": {
"user": {
"data": {
+ "api_key": "",
"latitude": "Latitude",
"longitude": "Longitude"
- }
+ },
+ "title": ""
}
}
}
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index 563e24bf8fd..d6d7a93a366 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -3,8 +3,13 @@ import asyncio
from datetime import timedelta
from math import ceil
-from pyairvisual import Client
-from pyairvisual.errors import AirVisualError, NodeProError
+from pyairvisual import CloudAPI, NodeSamba
+from pyairvisual.errors import (
+ AirVisualError,
+ InvalidKeyError,
+ KeyExpiredError,
+ NodeProError,
+)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@@ -206,29 +211,36 @@ 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],
)
try:
return await api_coro
+ except (InvalidKeyError, KeyExpiredError):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth"},
+ data=config_entry.data,
+ )
+ )
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@@ -254,17 +266,13 @@ async def async_setup_entry(hass, config_entry):
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 bb2d64a23db..047367fa67c 100644
--- a/homeassistant/components/airvisual/air_quality.py
+++ b/homeassistant/components/airvisual/air_quality.py
@@ -40,9 +40,9 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
@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 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
@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 abbc2df9061..d8ab508b8bc 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
@@ -34,12 +34,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+ def __init__(self):
+ """Initialize the config flow."""
+ self._geo_id = None
+ self._latitude = None
+ self._longitude = None
+
+ self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str})
+
@property
def geography_schema(self):
"""Return the data schema for the cloud API."""
- return vol.Schema(
+ return self.api_key_data_schema.extend(
{
- vol.Required(CONF_API_KEY): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
@@ -85,8 +92,8 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="geography", data_schema=self.geography_schema
)
- geo_id = async_get_geography_id(user_input)
- await self._async_set_unique_id(geo_id)
+ self._geo_id = async_get_geography_id(user_input)
+ await self._async_set_unique_id(self._geo_id)
self._abort_if_unique_id_configured()
# Find older config entries without unique ID:
@@ -95,13 +102,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
continue
if any(
- geo_id == async_get_geography_id(geography)
+ self._geo_id == async_get_geography_id(geography)
for geography in entry.data[CONF_GEOGRAPHIES]
):
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:
@@ -113,7 +120,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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",
@@ -123,10 +130,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
checked_keys.add(user_input[CONF_API_KEY])
- return self.async_create_entry(
- title=f"Cloud API ({geo_id})",
- data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
- )
+ return await self.async_step_geography_finish(user_input)
+
+ async def async_step_geography_finish(self, user_input=None):
+ """Handle the finalization of a Cloud API config entry."""
+ existing_entry = await self.async_set_unique_id(self._geo_id)
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_create_entry(
+ title=f"Cloud API ({self._geo_id})",
+ data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
+ )
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
@@ -141,16 +157,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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(
@@ -159,11 +169,37 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors={CONF_IP_ADDRESS: "unable_to_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},
)
+ async def async_step_reauth(self, data):
+ """Handle configuration by re-auth."""
+ self._latitude = data[CONF_LATITUDE]
+ self._longitude = data[CONF_LONGITUDE]
+
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """Handle re-auth completion."""
+ if not user_input:
+ return self.async_show_form(
+ step_id="reauth_confirm", data_schema=self.api_key_data_schema
+ )
+
+ conf = {
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ CONF_LATITUDE: self._latitude,
+ CONF_LONGITUDE: self._longitude,
+ }
+
+ self._geo_id = async_get_geography_id(conf)
+
+ return await self.async_step_geography_finish(conf)
+
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json
index 93b57a4804e..d7824551275 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 895ffa494a4..a81c118ecc9 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -38,10 +38,6 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
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 @@ class AirVisualNodeProSensor(AirVisualEntity):
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 @@ class AirVisualNodeProSensor(AirVisualEntity):
@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/translations/et.json b/homeassistant/components/airvisual/translations/et.json
new file mode 100644
index 00000000000..51bc3faf3f2
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "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/no.json b/homeassistant/components/airvisual/translations/no.json
index b9be5498560..8fcf00a6714 100644
--- a/homeassistant/components/airvisual/translations/no.json
+++ b/homeassistant/components/airvisual/translations/no.json
@@ -29,6 +29,7 @@
"user": {
"data": {
"cloud_api": "Geografisk plassering",
+ "node_pro": "",
"type": "Integrasjonstype"
},
"description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.",
diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json
index dea77a233aa..5851afceabd 100644
--- a/homeassistant/components/airvisual/translations/pl.json
+++ b/homeassistant/components/airvisual/translations/pl.json
@@ -4,7 +4,7 @@
"already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu."
},
"error": {
- "general_error": "Nieoczekiwany b\u0142\u0105d.",
+ "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/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py
index 81e444ae16f..0dc16fdcf42 100644
--- a/homeassistant/components/alarm_control_panel/device_action.py
+++ b/homeassistant/components/alarm_control_panel/device_action.py
@@ -6,6 +6,7 @@ import voluptuous as vol
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 c4d43d1b051..e5b3ec6aeee 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.components.alarm_control_panel.const import (
)
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 eeea1dbbf33..cb07ff35e96 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.automation import AutomationActionType
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 00000000000..6645f12245d
--- /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 28c47b5a06d..76b0c845d01 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 15b5fd8457c..0a0f33d6181 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 0aa9fcc29ec..8dd704f1333 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.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
+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,
+)
+
_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,
- }
-)
-
-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,
- }
-)
-
-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,
- }
-)
-
-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)
- device_type = device[CONF_DEVICE_TYPE]
- host = DEFAULT_DEVICE_HOST
- port = DEFAULT_DEVICE_PORT
- path = DEFAULT_DEVICE_PATH
- baud = DEFAULT_DEVICE_BAUD
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up AlarmDecoder config flow."""
+ undo_listener = entry.add_update_listener(_update_listener)
+
+ 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 setup(hass, config):
"""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 setup(hass, config):
controller.on_close += handle_closed_connection
controller.on_expander_message += handle_rel_message
- hass.data[DATA_AD] = controller
-
- open_connection()
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
-
- load_platform(
- hass,
- "alarm_control_panel",
- DOMAIN,
- {CONF_AUTO_BYPASS: auto_bypass, CONF_CODE_ARM_REQUIRED: code_arm_required},
- config,
+ remove_stop_listener = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder
)
- if zones:
- load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
+ 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,
+ }
- if display:
- load_platform(hass, "sensor", DOMAIN, conf, config)
+ await open_connection()
+
+ 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: HomeAssistantType, entry: ConfigEntry):
+ """Unload a AlarmDecoder entry."""
+ hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
+
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+
+ if not unload_ok:
+ return False
+
+ 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 117374552f3..bc2d74a5042 100644
--- a/homeassistant/components/alarmdecoder/alarm_control_panel.py
+++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py
@@ -12,6 +12,7 @@ from homeassistant.components.alarm_control_panel.const import (
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 @@ from homeassistant.const import (
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
+ options = entry.options
+ arm_options = options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
+ client = hass.data[DOMAIN][entry.entry_id][DATA_AD]
- 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])
+ 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 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
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 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
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 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
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 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
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 cec1b8356b0..89b7d00b3a4 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_LOOP4 = "rf_loop4"
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]
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+):
+ """Set up for AlarmDecoder sensor."""
- 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(
+ 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)
+ entities.append(entity)
- add_entities(devices)
-
- return True
+ async_add_entities(entities)
class AlarmDecoderBinarySensor(BinarySensorEntity):
@@ -67,7 +71,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
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
@@ -117,6 +121,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
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 00000000000..74b23f049a7
--- /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 00000000000..f1bfb66f0d4
--- /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 ea2c3fb01c8..1697858718d 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 96e5feb532d..4ce953af1d4 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 bcf5a927713..37c7ddf210c 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 00000000000..ed250b92b98
--- /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 00000000000..3042a991c5f
--- /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 00000000000..b42e092bb47
--- /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 00000000000..69318f87b11
--- /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 00000000000..ad488dd17ff
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/el.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "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"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "zone_details": {
+ "data": {
+ "zone_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \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"
+ },
+ "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 00000000000..66301414cc8
--- /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 00000000000..5b4670306da
--- /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 00000000000..4837483dee3
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/et.json
@@ -0,0 +1,49 @@
+{
+ "config": {
+ "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 00000000000..c48cf00cded
--- /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 00000000000..ca5bf39cefd
--- /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 00000000000..c4038572ece
--- /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 00000000000..08dc02d6ea7
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/lb.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "service_unavailable": "Feeler beim verbannen"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Apparat Baudrate",
+ "device_path": "Pad vum Apparat",
+ "host": "Host",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ },
+ "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"
+ },
+ "title": "AlarmDecoder konfigur\u00e9ieren"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Zone Nummer"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..6091d4c4bd7
--- /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 00000000000..36c5f21c60c
--- /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 00000000000..4e3f5b17ba1
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/pl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "edit_select": "Edytuj"
+ }
+ },
+ "zone_details": {
+ "data": {
+ "zone_relaychan": "Kana\u0142 przeka\u017anika"
+ }
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Numer strefy"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..3a6e56686fd
--- /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 00000000000..6c9f0dbcb43
--- /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 00000000000..4caf58203c8
--- /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/entities.py b/homeassistant/components/alexa/entities.py
index bd68e4ad926..53cf35dfe75 100644
--- a/homeassistant/components/alexa/entities.py
+++ b/homeassistant/components/alexa/entities.py
@@ -33,6 +33,7 @@ from homeassistant.const import (
CONF_NAME,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
+ __version__,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import network
@@ -286,6 +287,12 @@ class AlexaEntity:
"friendlyName": self.friendly_name(),
"description": self.description(),
"manufacturerName": "Home Assistant",
+ "additionalAttributes": {
+ "manufacturer": "Home Assistant",
+ "model": self.entity.domain,
+ "softwareVersion": __version__,
+ "customIdentifier": self.entity_id,
+ },
}
locale = self.config.locale
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 6eeb3235a64..783c7a36949 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -17,6 +17,7 @@ from homeassistant.components import (
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 a61dfc02d10..1d06422056d 100644
--- a/homeassistant/components/alexa/state_report.py
+++ b/homeassistant/components/alexa/state_report.py
@@ -6,7 +6,7 @@ import logging
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/strings.json b/homeassistant/components/almond/strings.json
index e8244798e81..9c85eeb92c1 100644
--- a/homeassistant/components/almond/strings.json
+++ b/homeassistant/components/almond/strings.json
@@ -1,7 +1,7 @@
{
"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}?"
diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json
index 8747f1ed7df..81f114be1eb 100644
--- a/homeassistant/components/almond/translations/ca.json
+++ b/homeassistant/components/almond/translations/ca.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.",
"cannot_connect": "No es pot connectar amb el servidor d'Almond.",
- "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond."
+ "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond.",
+ "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})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json
index 2a587d46403..01608d56faf 100644
--- a/homeassistant/components/almond/translations/en.json
+++ b/homeassistant/components/almond/translations/en.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
- "missing_configuration": "Please check the documentation on how to set up Almond."
+ "missing_configuration": "Please check the documentation on how to set up Almond.",
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json
index de9fb58eabd..94b1f4ea6ba 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 f39a1660bb9..7b7f4bff1e4 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 3e68336bf3e..6bf280230a4 100644
--- a/homeassistant/components/almond/translations/it.json
+++ b/homeassistant/components/almond/translations/it.json
@@ -3,7 +3,8 @@
"abort": {
"already_setup": "\u00c8 possibile configurare un solo account Almond.",
"cannot_connect": "Impossibile connettersi al server Almond.",
- "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond."
+ "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
},
"step": {
"hassio_confirm": {
@@ -11,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 645eaafab08..08cb120bf9d 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 3b866a326be..bfcdad87e52 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 7a2a60b1a69..da4671f8591 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 6e5c90b69e2..c9da3b2303c 100644
--- a/homeassistant/components/almond/translations/no.json
+++ b/homeassistant/components/almond/translations/no.json
@@ -3,11 +3,13 @@
"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": {
- "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?"
+ "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?",
+ "title": ""
},
"pick_implementation": {
"title": "Velg godkjenningsmetode"
diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json
index a4fa0f5d46c..3039ecc2d41 100644
--- a/homeassistant/components/almond/translations/ru.json
+++ b/homeassistant/components/almond/translations/ru.json
@@ -3,7 +3,8 @@
"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.",
"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 Almond.",
- "missing_configuration": "\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond."
+ "missing_configuration": "\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond.",
+ "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."
},
"step": {
"hassio_confirm": {
diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json
index 96e3d92e060..d5ea73a873d 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/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
index 9428449dc75..68bfb85cf62 100644
--- a/homeassistant/components/ambient_station/__init__.py
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -6,8 +6,10 @@ from aioambient import Client
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 @@ from homeassistant.const import (
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_WINDSPEEDMPH = "windspeedmph"
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 @@ SENSOR_TYPES = {
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 @@ SENSOR_TYPES = {
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/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py
index 14384565718..377ecfec667 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 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity):
@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/media_player.py b/homeassistant/components/androidtv/media_player.py
index 1ea20dbeca5..e64581f9a05 100644
--- a/homeassistant/components/androidtv/media_player.py
+++ b/homeassistant/components/androidtv/media_player.py
@@ -502,14 +502,23 @@ class ADBDevice(MediaPlayerEntity):
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/api/__init__.py b/homeassistant/components/api/__init__.py
index c1f692a76ad..34d899e996c 100644
--- a/homeassistant/components/api/__init__.py
+++ b/homeassistant/components/api/__init__.py
@@ -38,6 +38,7 @@ from homeassistant.helpers.json import JSONEncoder
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_BASE_URL = "base_url"
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 @@ class APIDiscoveryView(HomeAssistantView):
"""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 @@ class APIDiscoveryView(HomeAssistantView):
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 beb3a80ceeb..34061120322 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 f999da94531..95bf11ddc09 100644
--- a/homeassistant/components/apprise/notify.py
+++ b/homeassistant/components/apprise/notify.py
@@ -28,12 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Apprise notification service."""
-
- # Create our Apprise Asset Object
- asset = apprise.AppriseAsset(async_mode=False)
-
# Create our Apprise Instance (reference our asset)
- a_obj = apprise.Apprise(asset=asset)
+ a_obj = apprise.Apprise()
if config.get(CONF_FILE):
# Sourced from a Configuration File
diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json
index b067d24dc44..14d55224119 100644
--- a/homeassistant/components/arcam_fmj/translations/no.json
+++ b/homeassistant/components/arcam_fmj/translations/no.json
@@ -12,7 +12,8 @@
},
"user": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
}
diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json
index 7b2d5da76e5..42ed803a161 100644
--- a/homeassistant/components/arcam_fmj/translations/pl.json
+++ b/homeassistant/components/arcam_fmj/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"
},
"step": {
"user": {
diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py
index e87a625522e..2890fd4abda 100644
--- a/homeassistant/components/arduino/__init__.py
+++ b/homeassistant/components/arduino/__init__.py
@@ -23,6 +23,12 @@ CONFIG_SCHEMA = vol.Schema(
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/atag/manifest.json b/homeassistant/components/atag/manifest.json
index 9f8a5d2c6eb..5e94afb06d3 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/translations/nl.json b/homeassistant/components/atag/translations/nl.json
index ac6477ec4d2..077beb65871 100644
--- a/homeassistant/components/atag/translations/nl.json
+++ b/homeassistant/components/atag/translations/nl.json
@@ -16,5 +16,6 @@
"title": "Verbinding maken met het apparaat"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json
index aa2f7d1b3b8..a0e428f286a 100644
--- a/homeassistant/components/atag/translations/no.json
+++ b/homeassistant/components/atag/translations/no.json
@@ -11,10 +11,12 @@
"user": {
"data": {
"email": "E-post (valgfritt)",
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
"title": "Koble til enheten"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py
index e0d7749dcbb..feaf61450e8 100644
--- a/homeassistant/components/august/__init__.py
+++ b/homeassistant/components/august/__init__.py
@@ -3,13 +3,18 @@ import asyncio
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 @@ from .const import (
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 bf6f1d9cd81..f595479c0cf 100644
--- a/homeassistant/components/august/config_flow.py
+++ b/homeassistant/components/august/config_flow.py
@@ -4,7 +4,7 @@ import logging
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 @@ from .gateway import AugustGateway
_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 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""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 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 6918907611f..b72bb52e710 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 @@ class AugustGateway:
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 @@ class AugustGateway:
)
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 @@ class AugustGateway:
),
)
- 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 @@ class AugustGateway:
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 880c13c7fe2..254e8146984 100644
--- a/homeassistant/components/august/strings.json
+++ b/homeassistant/components/august/strings.json
@@ -6,7 +6,8 @@
"invalid_auth": "Invalid authentication"
},
"abort": {
- "already_configured": "Account is already configured"
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was 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 4f8f9cebe63..e8299249d73 100644
--- a/homeassistant/components/august/translations/ca.json
+++ b/homeassistant/components/august/translations/ca.json
@@ -1,7 +1,8 @@
{
"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",
diff --git a/homeassistant/components/august/translations/el.json b/homeassistant/components/august/translations/el.json
new file mode 100644
index 00000000000..08f214f7386
--- /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 b8bf1b1bc03..08d09e12095 100644
--- a/homeassistant/components/august/translations/en.json
+++ b/homeassistant/components/august/translations/en.json
@@ -1,7 +1,8 @@
{
"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",
diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json
index 28d9743c073..2ec72c9e4eb 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 00000000000..af21b9d8204
--- /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 752b7dc3712..82568b681fd 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 3a5f2676acd..8f88b34e5e4 100644
--- a/homeassistant/components/august/translations/it.json
+++ b/homeassistant/components/august/translations/it.json
@@ -1,7 +1,8 @@
{
"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.",
diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json
index 52f939c45a0..c11bc55ec40 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 501af05c2df..dbc71325a81 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 1697f634d9a..e48d27801cc 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 838508f132d..feb4e4e759b 100644
--- a/homeassistant/components/august/translations/no.json
+++ b/homeassistant/components/august/translations/no.json
@@ -1,7 +1,8 @@
{
"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",
diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json
index eeaa5269da4..0c38508399b 100644
--- a/homeassistant/components/august/translations/pl.json
+++ b/homeassistant/components/august/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/august/translations/ru.json b/homeassistant/components/august/translations/ru.json
index 9a49caed547..516b978a6e5 100644
--- a/homeassistant/components/august/translations/ru.json
+++ b/homeassistant/components/august/translations/ru.json
@@ -1,7 +1,8 @@
{
"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.",
diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json
index 6b7e206d4c4..56a7bc4ef95 100644
--- a/homeassistant/components/august/translations/zh-Hant.json
+++ b/homeassistant/components/august/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"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",
diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 31e3b7ea648..725450a0a12 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -80,7 +80,11 @@ from homeassistant.components.http.ban import (
)
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 @@ class LoginFlowIndexView(HomeAssistantView):
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/auth/translations/et.json b/homeassistant/components/auth/translations/et.json
new file mode 100644
index 00000000000..290f4ee12a9
--- /dev/null
+++ b/homeassistant/components/auth/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "mfa_setup": {
+ "totp": {
+ "title": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json
index d19140ee218..ea0f1baa067 100644
--- a/homeassistant/components/auth/translations/no.json
+++ b/homeassistant/components/auth/translations/no.json
@@ -28,7 +28,8 @@
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.",
"title": "Sett opp tofaktorautentisering ved hjelp av TOTP"
}
- }
+ },
+ "title": ""
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index 2ac2b8d9354..3a296178aeb 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -10,7 +10,7 @@ from homeassistant.config import async_log_exception, config_without_domain
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/no.json b/homeassistant/components/avri/translations/no.json
index 4fb4490ac88..5d7f77113b9 100644
--- a/homeassistant/components/avri/translations/no.json
+++ b/homeassistant/components/avri/translations/no.json
@@ -15,8 +15,10 @@
"house_number_extension": "Utvidelse av husnummer",
"zip_code": "Postnummer"
},
- "description": "Skriv inn adressen din"
+ "description": "Skriv inn adressen din",
+ "title": ""
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py
index e3c2a176119..b262fdec572 100644
--- a/homeassistant/components/awair/const.py
+++ b/homeassistant/components/awair/const.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
)
@@ -63,7 +64,7 @@ SENSOR_TYPES = {
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/translations/de.json b/homeassistant/components/awair/translations/de.json
index d0eb8e91b3e..fcdcd0190e3 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 00000000000..436e8b1fb7d
--- /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 07983402c42..76fe5a91cd9 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/translations/no.json b/homeassistant/components/axis/translations/no.json
index 70836d88239..039e6138753 100644
--- a/homeassistant/components/axis/translations/no.json
+++ b/homeassistant/components/axis/translations/no.json
@@ -18,6 +18,7 @@
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"title": "Sett opp Axis enhet"
diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json
index e6d55f5a579..154577573bb 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/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py
index e08dd4d8559..f72a4c44918 100644
--- a/homeassistant/components/azure_devops/__init__.py
+++ b/homeassistant/components/azure_devops/__init__.py
@@ -126,4 +126,5 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
},
"manufacturer": self.organization,
"name": self.project,
+ "entry_type": "service",
}
diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json
new file mode 100644
index 00000000000..528c76767ea
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/fr.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "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 00000000000..436e8b1fb7d
--- /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 00000000000..868cbe84645
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/pl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowany"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "organization": "Organizacja",
+ "project": "Projekt"
+ }
+ }
+ }
+ },
+ "title": "Azure DevOps"
+}
\ No newline at end of file
diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py
index df8e87f751d..4d4067eb77d 100644
--- a/homeassistant/components/bh1750/sensor.py
+++ b/homeassistant/components/bh1750/sensor.py
@@ -7,7 +7,7 @@ import smbus # pylint: disable=import-error
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 @@ OPERATION_MODES = {
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 00000000000..1636054663d
--- /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/ca.json b/homeassistant/components/binary_sensor/translations/ca.json
index bf16523251e..ee06ef9d87f 100644
--- a/homeassistant/components/binary_sensor/translations/ca.json
+++ b/homeassistant/components/binary_sensor/translations/ca.json
@@ -107,11 +107,11 @@
"on": "Connectat"
},
"door": {
- "off": "Tancat/da",
+ "off": "Tancat/ada",
"on": "Obert/a"
},
"garage_door": {
- "off": "Tancat/da",
+ "off": "Tancat/ada",
"on": "Obert/a"
},
"gas": {
@@ -139,7 +139,7 @@
"on": "Detectat"
},
"opening": {
- "off": "Tancat/da",
+ "off": "Tancat/ada",
"on": "Obert/a"
},
"presence": {
@@ -167,7 +167,7 @@
"on": "Detectat"
},
"window": {
- "off": "Tancat/da",
+ "off": "Tancat/ada",
"on": "Obert/a"
}
},
diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json
index a9da1be9ee2..c074c56675a 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/nb.json b/homeassistant/components/binary_sensor/translations/nb.json
index 8b143f7499a..76c56713646 100644
--- a/homeassistant/components/binary_sensor/translations/nb.json
+++ b/homeassistant/components/binary_sensor/translations/nb.json
@@ -9,6 +9,7 @@
"on": "Lavt"
},
"cold": {
+ "off": "",
"on": "Kald"
},
"connectivity": {
@@ -55,6 +56,10 @@
"off": "Borte",
"on": "Hjemme"
},
+ "problem": {
+ "off": "",
+ "on": ""
+ },
"safety": {
"off": "Sikker",
"on": "Usikker"
diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json
index 25b7c165c11..b78a50a8628 100644
--- a/homeassistant/components/binary_sensor/translations/no.json
+++ b/homeassistant/components/binary_sensor/translations/no.json
@@ -99,6 +99,7 @@
"on": "Lavt"
},
"cold": {
+ "off": "",
"on": "Kald"
},
"connectivity": {
@@ -118,6 +119,7 @@
"on": "Oppdaget"
},
"heat": {
+ "off": "",
"on": "Varm"
},
"lock": {
@@ -144,6 +146,10 @@
"off": "Borte",
"on": "Hjemme"
},
+ "problem": {
+ "off": "",
+ "on": ""
+ },
"safety": {
"off": "Sikker",
"on": "Usikker"
diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json
index 7b01acae4fb..29767f6d6d6 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/translations/fr.json b/homeassistant/components/blebox/translations/fr.json
index 75d506a8212..d30d026d177 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/no.json b/homeassistant/components/blebox/translations/no.json
index 925f680107e..239d1fb03c6 100644
--- a/homeassistant/components/blebox/translations/no.json
+++ b/homeassistant/components/blebox/translations/no.json
@@ -13,7 +13,8 @@
"step": {
"user": {
"data": {
- "host": "IP adresse"
+ "host": "IP adresse",
+ "port": ""
},
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
"title": "Konfigurere BleBox-enheten"
diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json
index 80468b36409..83aaad902a1 100644
--- a/homeassistant/components/blink/translations/fr.json
+++ b/homeassistant/components/blink/translations/fr.json
@@ -4,6 +4,8 @@
"already_configured": "P\u00e9riph\u00e9rique d\u00e9j\u00e0 configur\u00e9"
},
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_access_token": "Jeton d'acc\u00e8s non valide",
"invalid_auth": "Authentification invalide",
"unknown": "Erreur inattendue"
},
diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json
index ef3ffc108e5..ac8c96e4f2d 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 7d6d01266d9..1564ed2d685 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": {
diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py
index 929f8218144..cd993e0332a 100644
--- a/homeassistant/components/bloomsky/__init__.py
+++ b/homeassistant/components/bloomsky/__init__.py
@@ -6,7 +6,12 @@ from aiohttp.hdrs import AUTHORIZATION
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 @@ class BloomSky:
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 077171006bf..43c5614679c 100644
--- a/homeassistant/components/bloomsky/binary_sensor.py
+++ b/homeassistant/components/bloomsky/binary_sensor.py
@@ -3,7 +3,11 @@ import logging
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 @@ from . import DOMAIN
_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 0ddeec6a577..eaa03ef2f3b 100644
--- a/homeassistant/components/bloomsky/sensor.py
+++ b/homeassistant/components/bloomsky/sensor.py
@@ -5,8 +5,11 @@ import voluptuous as vol
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_TYPES = [
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_IMPERIAL = {
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 ee89873e8fe..31ef2dacf3a 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -3,7 +3,12 @@ import logging
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 @@ from .const import ATTRIBUTION
_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 56406b29e82..12f43430829 100644
--- a/homeassistant/components/bom/sensor.py
+++ b/homeassistant/components/bom/sensor.py
@@ -21,7 +21,9 @@ from homeassistant.const import (
CONF_NAME,
LENGTH_KILOMETERS,
LENGTH_METERS,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
+ PRESSURE_MBAR,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
)
@@ -66,11 +68,11 @@ SENSOR_TYPES = {
"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],
diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py
index 49ca559685c..6666cd57ca3 100644
--- a/homeassistant/components/bond/config_flow.py
+++ b/homeassistant/components/bond/config_flow.py
@@ -7,7 +7,12 @@ from bond_api import Bond
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 19e345b7e23..e59d0234beb 100644
--- a/homeassistant/components/bond/fan.py
+++ b/homeassistant/components/bond/fan.py
@@ -120,7 +120,7 @@ class BondFan(BondEntity, FanEntity):
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 7dec44dbb38..5e66019579d 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 .const import DOMAIN
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 @@ class BondFireplace(BondEntity, LightEntity):
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 @@ class BondFireplace(BondEntity, LightEntity):
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 d10ea1e71b0..393232025dd 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 74beceeccd9..496a21339cb 100644
--- a/homeassistant/components/bond/translations/fr.json
+++ b/homeassistant/components/bond/translations/fr.json
@@ -1,11 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
"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})",
"step": {
+ "confirm": {
+ "data": {
+ "access_token": "Jeton d'acc\u00e8s"
+ },
+ "description": "Voulez-vous configurer {bond_id} ?"
+ },
"user": {
"data": {
"access_token": "Token d'acc\u00e8s",
diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 d50380c81eb..61576d70431 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 10b6433daee..4fae986f701 100644
--- a/homeassistant/components/bond/translations/pl.json
+++ b/homeassistant/components/bond/translations/pl.json
@@ -1,11 +1,19 @@
{
"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",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
+ "confirm": {
+ "data": {
+ "access_token": "Token dost\u0119pu"
+ }
+ },
"user": {
"data": {
"access_token": "Token dost\u0119pu",
diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json
index ad5907b4678..cd687d8f2d0 100644
--- a/homeassistant/components/braviatv/translations/no.json
+++ b/homeassistant/components/braviatv/translations/no.json
@@ -21,7 +21,8 @@
"data": {
"host": "Vert"
},
- "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5."
+ "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.",
+ "title": ""
}
}
},
diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py
index 4dfc80c6fe9..9fe83e350cf 100644
--- a/homeassistant/components/broadlink/config_flow.py
+++ b/homeassistant/components/broadlink/config_flow.py
@@ -7,7 +7,7 @@ import broadlink as blk
from broadlink.exceptions import (
AuthenticationError,
BroadlinkException,
- DeviceOfflineError,
+ NetworkTimeoutError,
)
import voluptuous as vol
@@ -139,7 +139,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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)
@@ -207,7 +207,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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 d05fdfd4df6..51d9b0a497f 100644
--- a/homeassistant/components/broadlink/device.py
+++ b/homeassistant/components/broadlink/device.py
@@ -9,7 +9,7 @@ from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
ConnectionClosedError,
- DeviceOfflineError,
+ NetworkTimeoutError,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
@@ -82,7 +82,7 @@ class BroadlinkDevice:
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 6a630044904..9c6e571ec86 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 7c1ae7349da..32305331a21 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -9,7 +9,7 @@ import logging
from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
- DeviceOfflineError,
+ NetworkTimeoutError,
ReadError,
StorageError,
)
@@ -262,7 +262,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
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
@@ -295,7 +295,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
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/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json
index 3e642b0f6b5..4d2fa7a8373 100644
--- a/homeassistant/components/broadlink/translations/ca.json
+++ b/homeassistant/components/broadlink/translations/ca.json
@@ -5,6 +5,7 @@
"already_in_progress": "Ja hi ha un flux de configuraci\u00f3 en curs per a aquest dispositiu",
"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 00000000000..b0d7a55c787
--- /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 00000000000..d3346614257
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/el.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json
index fdceeabd2cd..98c4cbdfb30 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 00000000000..fc7f3424b62
--- /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 2bf2477f615..1d80059fb7a 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 00000000000..3b2d79a34a7
--- /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 939925104fd..7f26e33e14f 100644
--- a/homeassistant/components/broadlink/translations/it.json
+++ b/homeassistant/components/broadlink/translations/it.json
@@ -5,6 +5,7 @@
"already_in_progress": "\u00c8 gi\u00e0 in corso un flusso di configurazione per questo dispositivo",
"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 00000000000..47ebf3db64a
--- /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 0872c01b608..e4f60a3eac9 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 00000000000..d6185150e49
--- /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 beeae80745d..14b55c93a96 100644
--- a/homeassistant/components/broadlink/translations/no.json
+++ b/homeassistant/components/broadlink/translations/no.json
@@ -5,6 +5,7 @@
"already_in_progress": "Det p\u00e5g\u00e5r allerede en konfigurasjonsflyt for denne enheten",
"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 a168d020d02..64afc759689 100644
--- a/homeassistant/components/broadlink/translations/pl.json
+++ b/homeassistant/components/broadlink/translations/pl.json
@@ -24,7 +24,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/pt.json b/homeassistant/components/broadlink/translations/pt.json
index c1b7a83d443..bf246b55b36 100644
--- a/homeassistant/components/broadlink/translations/pt.json
+++ b/homeassistant/components/broadlink/translations/pt.json
@@ -11,6 +11,21 @@
},
"flow_title": "{name} ({model} em {host})",
"step": {
+ "auth": {
+ "title": "Autenticar no dispositivo"
+ },
+ "finish": {
+ "data": {
+ "name": "Nome"
+ },
+ "title": "Escolha um nome para o dispositivo"
+ },
+ "reset": {
+ "title": "Desbloqueie o dispositivo"
+ },
+ "unlock": {
+ "title": "Desbloqueie o dispositivo (opcional)"
+ },
"user": {
"data": {
"host": "Servidor"
diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json
index f7d0cfab3d1..542efe753f9 100644
--- a/homeassistant/components/broadlink/translations/ru.json
+++ b/homeassistant/components/broadlink/translations/ru.json
@@ -5,6 +5,7 @@
"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.",
"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 00000000000..38d02e42d90
--- /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 741e8beb2f7..6bb8d54bd16 100644
--- a/homeassistant/components/broadlink/translations/zh-Hant.json
+++ b/homeassistant/components/broadlink/translations/zh-Hant.json
@@ -5,6 +5,7 @@
"already_in_progress": "\u6b64\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"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 8b6f1316f52..d36a07a185b 100644
--- a/homeassistant/components/broadlink/updater.py
+++ b/homeassistant/components/broadlink/updater.py
@@ -9,7 +9,7 @@ from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
CommandNotSupportedError,
- DeviceOfflineError,
+ NetworkTimeoutError,
StorageError,
)
@@ -113,7 +113,7 @@ class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager):
)
devices = await self.device.hass.async_add_executor_job(hello)
if not devices:
- raise DeviceOfflineError("The device is offline")
+ raise NetworkTimeoutError("The device is offline")
return {}
diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json
new file mode 100644
index 00000000000..4c5bb032f0b
--- /dev/null
+++ b/homeassistant/components/brother/translations/et.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "unsupported_model": "Seda printeri mudelit ei toetata."
+ },
+ "error": {
+ "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/brunt/cover.py b/homeassistant/components/brunt/cover.py
index 7f36874c40e..ceb56ba03fa 100644
--- a/homeassistant/components/brunt/cover.py
+++ b/homeassistant/components/brunt/cover.py
@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.cover import (
ATTR_POSITION,
+ DEVICE_CLASS_WINDOW,
PLATFORM_SCHEMA,
SUPPORT_CLOSE,
SUPPORT_OPEN,
@@ -19,7 +20,6 @@ import homeassistant.helpers.config_validation as cv
_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 @@ class BruntDevice(CoverEntity):
@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/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json
index c1909b19508..c92ccbebb62 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 319fbb771af..040349997f4 100644
--- a/homeassistant/components/bsblan/translations/no.json
+++ b/homeassistant/components/bsblan/translations/no.json
@@ -11,7 +11,8 @@
"user": {
"data": {
"host": "Vert",
- "passkey": "Tilgangsn\u00f8kkel streng"
+ "passkey": "Tilgangsn\u00f8kkel streng",
+ "port": ""
},
"description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.",
"title": "Koble til BSB-Lan-enheten"
diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json
index 21b42085761..d8a30744659 100644
--- a/homeassistant/components/bsblan/translations/pl.json
+++ b/homeassistant/components/bsblan/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": {
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem BSB_LAN."
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index ba0063ebb85..b1e41122dc0 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -30,7 +30,9 @@ from homeassistant.const import (
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 @@ SENSOR_TYPES = {
"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 @@ SENSOR_TYPES = {
"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 f4950751fc9..25505800709 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -36,6 +36,7 @@ from homeassistant.components.stream.const import (
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_FILENAME,
+ CONTENT_TYPE_MULTIPART,
EVENT_HOMEASSISTANT_START,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -185,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):
diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py
index 165787a7f46..06f4134e24e 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 canary.api import Api
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.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
+from .const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_CANARY,
+ DATA_UNDO_UPDATE_LISTENER,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+
_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,48 +41,114 @@ CONFIG_SCHEMA = vol.Schema(
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, {})
+
+ if hass.config_entries.async_entries(DOMAIN):
+ return True
+
+ 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
+
+
+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:
- 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,
+ canary_data = await hass.async_add_executor_job(
+ _get_canary_data_instance, entry
)
- return False
+ except (ConnectTimeout, HTTPError) as error:
+ _LOGGER.error("Unable to connect to Canary service: %s", str(error))
+ raise ConfigEntryNotReady from error
- for component in CANARY_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ undo_listener = entry.add_update_listener(_async_update_listener)
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_CANARY: canary_data,
+ DATA_UNDO_UPDATE_LISTENER: undo_listener,
+ }
+
+ 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: 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
+ ]
+ )
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None:
+ """Handle options update."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
+
class CanaryData:
- """Get the latest data and update the states."""
+ """Manages the data retrieved from Canary API."""
- def __init__(self, username, password, timeout):
+ def __init__(self, api: Api):
"""Init the Canary data object."""
-
- self._api = Api(username, password, timeout)
-
+ self._api = api
self._locations_by_id = {}
self._readings_by_device_id = {}
- self.update()
-
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
+ """Get the latest data from py-canary with a throttle."""
+ self._update(**kwargs)
+
+ def _update(self, **kwargs):
"""Get the latest data from py-canary."""
for location in self._api.get_locations():
location_id = location.location_id
@@ -121,3 +194,17 @@ class CanaryData:
def get_live_stream_session(self, device):
"""Return live stream session."""
return self._api.get_live_stream_session(device)
+
+
+def _get_canary_data_instance(entry: ConfigEntry) -> CanaryData:
+ """Initialize a new instance of CanaryData."""
+ canary = Api(
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
+ entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ )
+
+ canary_data = CanaryData(canary)
+ canary_data.update()
+
+ return canary_data
diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py
index ea0e3078b0c..8d2b01fd5da 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,24 +10,32 @@ from homeassistant.components.alarm_control_panel.const import (
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 . import DATA_CANARY
+from . import CanaryData
+from .const import DATA_CANARY, DOMAIN
_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."""
+ data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY]
+ alarms = [CanaryAlarm(data, location.location_id) for location in data.locations]
- add_entities(devices, True)
+ async_add_entities(alarms, True)
class CanaryAlarm(AlarmControlPanelEntity):
@@ -43,6 +52,11 @@ class CanaryAlarm(AlarmControlPanelEntity):
location = self._data.get_location(self._location_id)
return 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."""
diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py
index 3ba7f094da1..1cc7a535344 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,47 +10,67 @@ import voluptuous as vol
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.util import Throttle
-from . import DATA_CANARY, DEFAULT_TIMEOUT
+from . import CanaryData
+from .const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_CANARY,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+ MANUFACTURER,
+)
_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
+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."""
+ data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY]
- data = hass.data[DATA_CANARY]
- devices = []
+ ffmpeg_arguments = entry.options.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ )
+ cameras = []
for location in data.locations:
for device in location.devices:
if device.is_online:
- devices.append(
+ cameras.append(
CanaryCamera(
hass,
data,
location,
device,
DEFAULT_TIMEOUT,
- config[CONF_FFMPEG_ARGUMENTS],
+ ffmpeg_arguments,
)
)
- add_entities(devices, True)
+ async_add_entities(cameras, True)
class CanaryCamera(Camera):
@@ -64,13 +85,31 @@ class CanaryCamera(Camera):
self._data = data
self._location = location
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 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):
diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py
new file mode 100644
index 00000000000..dc2822d836a
--- /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 00000000000..e6e3dbb73c9
--- /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_CANARY = "canary"
+DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
+
+# Defaults
+DEFAULT_FFMPEG_ARGUMENTS = "-pred 1"
+DEFAULT_TIMEOUT = 10
diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json
index e383cb7514b..b4598d64087 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 5f1b1fe906b..acf44457cbf 100644
--- a/homeassistant/components/canary/sensor.py
+++ b/homeassistant/components/canary/sensor.py
@@ -1,11 +1,22 @@
"""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,
+ TEMP_CELSIUS,
+)
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.icon import icon_for_battery_level
+from homeassistant.helpers.typing import HomeAssistantType
-from . import DATA_CANARY
+from . import CanaryData
+from .const import DATA_CANARY, DOMAIN, MANUFACTURER
SENSOR_VALUE_PRECISION = 2
ATTR_AIR_QUALITY = "air_quality"
@@ -18,13 +29,13 @@ CANARY_PRO = "Canary Pro"
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", "dBm", None, DEVICE_CLASS_SIGNAL_STRENGTH, [CANARY_FLEX]],
+ ["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]],
]
STATE_AIR_QUALITY_NORMAL = "normal"
@@ -32,22 +43,26 @@ STATE_AIR_QUALITY_ABNORMAL = "abnormal"
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."""
+ data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY]
+ sensors = []
for location in data.locations:
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(
+ if device_type.get("name") in sensor_type[4]:
+ sensors.append(
CanarySensor(data, sensor_type, location, device)
)
- add_entities(devices, True)
+ async_add_entities(sensors, True)
class CanarySensor(Entity):
@@ -58,6 +73,8 @@ class CanarySensor(Entity):
self._data = data
self._sensor_type = sensor_type
self._device_id = device.device_id
+ self._device_name = device.name
+ self._device_type_name = device.device_type["name"]
self._sensor_value = None
sensor_type_name = sensor_type[0].replace("_", " ").title()
@@ -78,17 +95,29 @@ class CanarySensor(Entity):
"""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
diff --git a/homeassistant/components/canary/strings.json b/homeassistant/components/canary/strings.json
new file mode 100644
index 00000000000..504a5dc2ac1
--- /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 00000000000..c4b80d7537a
--- /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 00000000000..159f961c3a6
--- /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 00000000000..2cf87db92a3
--- /dev/null
+++ b/homeassistant/components/canary/translations/el.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ }
+ }
+}
\ 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 00000000000..1e04d1825f3
--- /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 00000000000..1b881d4dcd2
--- /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 00000000000..cc6601e6cc5
--- /dev/null
+++ b/homeassistant/components/canary/translations/et.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus"
+ },
+ "flow_title": "Canary {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "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 00000000000..9bb1761f9fb
--- /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 00000000000..b29a758acaa
--- /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 00000000000..0b1d82bb20a
--- /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 00000000000..0ad059e1fcc
--- /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 00000000000..9681bcd7c37
--- /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 00000000000..1de0a59b206
--- /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 00000000000..1d8bfdf512d
--- /dev/null
+++ b/homeassistant/components/canary/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..146863cf768
--- /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 00000000000..07463bc8a15
--- /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/media_player.py b/homeassistant/components/cast/media_player.py
index 788da18e8bd..76b6bd17f40 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -86,7 +86,7 @@ SUPPORT_CAST = (
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,
@@ -97,7 +97,7 @@ ENTITY_SCHEMA = vol.All(
)
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,
diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json
new file mode 100644
index 00000000000..05287b5a52b
--- /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/cert_expiry/translations/no.json b/homeassistant/components/cert_expiry/translations/no.json
index 5832e0a0dd5..a7aa3d1ab13 100644
--- a/homeassistant/components/cert_expiry/translations/no.json
+++ b/homeassistant/components/cert_expiry/translations/no.json
@@ -13,7 +13,8 @@
"user": {
"data": {
"host": "Vert",
- "name": "Sertifikatets navn"
+ "name": "Sertifikatets navn",
+ "port": ""
},
"title": "Definer sertifikatet som skal testes"
}
diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py
index 966dbdee6e2..09096f44b74 100644
--- a/homeassistant/components/clickatell/notify.py
+++ b/homeassistant/components/clickatell/notify.py
@@ -5,7 +5,7 @@ import requests
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 @@ class ClickatellNotificationService(BaseNotificationService):
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 6f7725ac835..3f2b8dc23f2 100644
--- a/homeassistant/components/climate/device_action.py
+++ b/homeassistant/components/climate/device_action.py
@@ -5,6 +5,7 @@ import voluptuous as vol
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 8a5b9ceede8..423efdf8196 100644
--- a/homeassistant/components/climate/device_condition.py
+++ b/homeassistant/components/climate/device_condition.py
@@ -5,6 +5,7 @@ import voluptuous as vol
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 00000000000..87674da414b
--- /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 1c4a6a5ff11..7be57f4cdaa 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/no.json b/homeassistant/components/climate/translations/no.json
index 3117378191d..4ac58d07bbb 100644
--- a/homeassistant/components/climate/translations/no.json
+++ b/homeassistant/components/climate/translations/no.json
@@ -16,6 +16,7 @@
},
"state": {
"_": {
+ "auto": "",
"cool": "Kj\u00f8le",
"dry": "T\u00f8rr",
"fan_only": "Kun vifte",
diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json
index 227e0e1f4ef..8d636c386e5 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 baa63679d42..0e3b20fa011 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 @@ class CloudRemoteBinary(BinarySensorEntity):
@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 00a2ddb4663..3075f6a3f9d 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -19,7 +19,13 @@ from homeassistant.components.google_assistant import helpers as google_helpers
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 @@ _CLOUD_ERRORS = {
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 6ec92477555..5f04ba134a8 100644
--- a/homeassistant/components/comfoconnect/sensor.py
+++ b/homeassistant/components/comfoconnect/sensor.py
@@ -34,6 +34,7 @@ from homeassistant.const import (
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 @@ SENSOR_TYPES = {
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/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py
index 3077056c397..1288b9472ed 100644
--- a/homeassistant/components/concord232/binary_sensor.py
+++ b/homeassistant/components/concord232/binary_sensor.py
@@ -7,6 +7,10 @@ import requests
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):
diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py
index e26b2b80bc1..22a9bf4f02a 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 async_setup(hass):
)
)
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 edc2e9af42c..7a99d29c774 100644
--- a/homeassistant/components/config/zwave.py
+++ b/homeassistant/components/config/zwave.py
@@ -6,7 +6,7 @@ from aiohttp.web import Response
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 @@ class ZWaveProtectionView(HomeAssistantView):
)
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 00000000000..1653a11c3ed
--- /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
new file mode 100644
index 00000000000..7d9bd88a810
--- /dev/null
+++ b/homeassistant/components/control4/translations/fr.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..4d00f0bfc74
--- /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 3064a0044b1..0076cfb69dd 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/coronavirus/translations/et.json b/homeassistant/components/coronavirus/translations/et.json
new file mode 100644
index 00000000000..a69b845e623
--- /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/cover/group.py b/homeassistant/components/cover/group.py
new file mode 100644
index 00000000000..d031b7cf693
--- /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/ca.json b/homeassistant/components/cover/translations/ca.json
index d033606abdd..3c4cb0c8b1b 100644
--- a/homeassistant/components/cover/translations/ca.json
+++ b/homeassistant/components/cover/translations/ca.json
@@ -10,7 +10,7 @@
"stop": "Atura {entity_name}"
},
"condition_type": {
- "is_closed": "{entity_name} est\u00e0 tancat/da",
+ "is_closed": "{entity_name} est\u00e0 tancat/ada",
"is_closing": "{entity_name} est\u00e0 tancant-se",
"is_open": "{entity_name} est\u00e0 obert/a",
"is_opening": "{entity_name} s'est\u00e0 obrint",
@@ -18,7 +18,7 @@
"is_tilt_position": "La inclinaci\u00f3 actual de {entity_name} \u00e9s"
},
"trigger_type": {
- "closed": "{entity_name} tancat/da",
+ "closed": "{entity_name} tancat/ada",
"closing": "{entity_name} tancant-se",
"opened": "{entity_name} s'ha obert",
"opening": "{entity_name} obrint-se",
@@ -28,7 +28,7 @@
},
"state": {
"_": {
- "closed": "Tancat/da",
+ "closed": "Tancat/ada",
"closing": "Tancant",
"open": "Obert/a",
"opening": "Obrint",
diff --git a/homeassistant/components/cover/translations/et.json b/homeassistant/components/cover/translations/et.json
index 96d81b3a7b6..baca6feeca5 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/fr.json b/homeassistant/components/cover/translations/fr.json
index 31cb82a6e7b..92cd9b223ea 100644
--- a/homeassistant/components/cover/translations/fr.json
+++ b/homeassistant/components/cover/translations/fr.json
@@ -5,7 +5,9 @@
"close_tilt": "Fermer {entity_name}",
"open": "Ouvrir {entity_name}",
"open_tilt": "Ouvrir {entity_name}",
- "set_position": "D\u00e9finir la position de {entity_name}"
+ "set_position": "D\u00e9finir la position de {entity_name}",
+ "set_tilt_position": "D\u00e9finir la position d'inclinaison de {entity_name}",
+ "stop": "Arr\u00eater {entity_name}"
},
"condition_type": {
"is_closed": "{entity_name} est ferm\u00e9",
diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json
index 0485a9bb371..66cd0c77c73 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/cover/translations/zh-Hant.json b/homeassistant/components/cover/translations/zh-Hant.json
index a8752b13f00..d23d4778809 100644
--- a/homeassistant/components/cover/translations/zh-Hant.json
+++ b/homeassistant/components/cover/translations/zh-Hant.json
@@ -10,9 +10,9 @@
"stop": "\u505c\u6b62 {entity_name}"
},
"condition_type": {
- "is_closed": "{entity_name}\u5df2\u95dc\u9589",
+ "is_closed": "{entity_name}\u70ba\u95dc\u9589",
"is_closing": "{entity_name}\u6b63\u5728\u95dc\u9589",
- "is_open": "{entity_name}\u5df2\u958b\u555f",
+ "is_open": "{entity_name}\u70ba\u958b\u555f",
"is_opening": "{entity_name}\u6b63\u5728\u958b\u555f",
"is_position": "\u76ee\u524d{entity_name}\u4f4d\u7f6e\u70ba",
"is_tilt_position": "\u76ee\u524d{entity_name}\u6a19\u984c\u4f4d\u7f6e\u70ba"
diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json
index fc39f78c2d0..7b40fa55a1d 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 7f6876a709b..9d3123185c4 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 e167f16b4e4..49b5c6c69f6 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -18,7 +18,9 @@ from homeassistant.const import (
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 @@ SENSOR_TYPES = {
],
"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 @@ SENSOR_TYPES = {
],
"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 @@ SENSOR_TYPES = {
],
"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 9b6fd1bdb64..c324d6a5b64 100644
--- a/homeassistant/components/ddwrt/device_tracker.py
+++ b/homeassistant/components/ddwrt/device_tracker.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
)
import homeassistant.helpers.config_validation as cv
@@ -155,7 +156,7 @@ class DdWrtDeviceScanner(DeviceScanner):
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 c8917934dd7..f7a9c1a5217 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OPENING,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_VIBRATION,
+ DOMAIN,
BinarySensorEntity,
)
from homeassistant.const import ATTR_TEMPERATURE
@@ -39,17 +40,18 @@ 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 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")
@@ -73,15 +75,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
"""Representation of a deCONZ binary sensor."""
- @callback
- def async_update_callback(self, force_update=False, ignore_update=False):
- """Update the sensor's state."""
- if ignore_update:
- return
+ TYPE = DOMAIN
+ @callback
+ def async_update_callback(self, force_update=False):
+ """Update the sensor's state."""
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 424693505ca..10ea7173f8a 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,
@@ -29,17 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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")
@@ -61,6 +62,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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 f3ae5682131..6c2df3ad614 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -22,13 +22,13 @@ from homeassistant.helpers import aiohttp_client
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"
@@ -251,18 +251,17 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow):
"""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 +270,15 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow):
{
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 c2190321fdf..31ae9eb018c 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -11,9 +11,11 @@ CONF_GROUP_ID_BASE = "group_id_base"
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 = [
@@ -44,4 +46,6 @@ 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 e01cfdbe5f8..7bcd821a344 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,
@@ -22,9 +24,10 @@ 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 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,7 +35,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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)
@@ -49,6 +55,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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 +82,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
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 b77014cc34b..4bcd63c8fa2 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -14,7 +14,6 @@ class DeconzBase:
"""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 @@ class DeconzBase:
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 @@ class DeconzDevice(DeconzBase, Entity):
"""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 @@ class DeconzDevice(DeconzBase, Entity):
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 9ad4a7f3162..968ab3cee39 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 @@ class DeconzEvent(DeconzBase):
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 @@ class DeconzEvent(DeconzBase):
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 bddde34a7de..0bff3f17f9e 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -267,7 +267,8 @@ AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = {
}
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 +371,8 @@ REMOTES = {
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,
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 828f65c9811..5ec07c40754 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -14,9 +14,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
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 @@ from .const import (
NEW_SENSOR,
SUPPORTED_PLATFORMS,
)
+from .deconz_event import async_setup_events, async_unload_events
from .errors import AuthenticationRequired, CannotConnect
@@ -42,9 +45,14 @@ class DeconzGateway:
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 @@ class DeconzGateway:
"""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 @@ class DeconzGateway:
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 @@ class DeconzGateway:
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 @@ class DeconzGateway:
)
)
+ 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 @@ class DeconzGateway:
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 @@ class DeconzGateway:
# 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 @@ class DeconzGateway:
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 fc0c01b30df..3f11cef31da 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -6,6 +6,7 @@ from homeassistant.components.light import (
ATTR_FLASH,
ATTR_HS_COLOR,
ATTR_TRANSITION,
+ DOMAIN,
EFFECT_COLORLOOP,
FLASH_LONG,
FLASH_SHORT,
@@ -40,6 +41,7 @@ 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 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,7 +49,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
for light in lights:
- if light.type not in COVER_TYPES + SWITCH_TYPES:
+ if (
+ light.type not in COVER_TYPES + SWITCH_TYPES
+ and light.uniqueid not in gateway.entities[DOMAIN]
+ ):
entities.append(DeconzLight(light, gateway))
async_add_entities(entities, True)
@@ -67,8 +72,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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)
@@ -85,6 +95,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DeconzBaseLight(DeconzDevice, LightEntity):
"""Representation of a deCONZ light."""
+ TYPE = DOMAIN
+
def __init__(self, device, gateway):
"""Set up light."""
super().__init__(device, gateway)
@@ -223,14 +235,13 @@ class DeconzGroup(DeconzBaseLight):
def __init__(self, device, gateway):
"""Set up group and create an unique id."""
+ 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}"
+
super().__init__(device, gateway)
- 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}"
-
@property
def unique_id(self):
"""Return a unique identifier for this device."""
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 4ebed981e2d..08e81d2dd3b 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -12,6 +12,7 @@ from pydeconz.sensor import (
Thermostat,
)
+from homeassistant.components.sensor import DOMAIN
from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
@@ -22,6 +23,7 @@ from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_HPA,
@@ -35,7 +37,6 @@ from homeassistant.helpers.dispatcher import (
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,7 +61,7 @@ ICON = {
UNIT_OF_MEASUREMENT = {
Consumption: ENERGY_KILO_WATT_HOUR,
Humidity: PERCENTAGE,
- LightLevel: "lx",
+ LightLevel: LIGHT_LUX,
Power: POWER_WATT,
Pressure: PRESSURE_HPA,
Temperature: TEMP_CELSIUS,
@@ -74,52 +75,43 @@ 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 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)
+ 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))
+
async_add_entities(entities, True)
gateway.listeners.append(
@@ -136,15 +128,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
- @callback
- def async_update_callback(self, force_update=False, ignore_update=False):
- """Update the sensor's state."""
- if ignore_update:
- return
+ TYPE = DOMAIN
+ @callback
+ def async_update_callback(self, force_update=False):
+ """Update the sensor's state."""
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 +192,14 @@ class DeconzSensor(DeconzDevice):
class DeconzBattery(DeconzDevice):
"""Battery class for when a device is only represented as an event."""
- @callback
- def async_update_callback(self, force_update=False, ignore_update=False):
- """Update the battery's state, if needed."""
- if ignore_update:
- return
+ TYPE = DOMAIN
+ @callback
+ def async_update_callback(self, force_update=False):
+ """Update the battery's state, if needed."""
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 +263,6 @@ class DeconzSensorStateTracker:
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 c85fa8073a3..bf503376321 100644
--- a/homeassistant/components/deconz/services.py
+++ b/homeassistant/components/deconz/services.py
@@ -3,6 +3,10 @@ from pydeconz.utils import normalize_bridge_id
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_CONFIGURE_DEVICE_SCHEMA = vol.All(
)
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_setup_services(hass):
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_setup_services(hass):
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 d8bf3e4d994..9d85e76d8d3 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 cf5acb5cff7..d3e9e884cb2 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -39,7 +39,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 +101,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 d7b6b55fbb8..dacae4d4a56 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
@@ -15,9 +15,10 @@ 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 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,10 +27,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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)
@@ -46,6 +52,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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 +73,8 @@ class DeconzPowerPlug(DeconzDevice, SwitchEntity):
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 1c8500b3cc1..bb09ff892d6 100644
--- a/homeassistant/components/deconz/translations/ca.json
+++ b/homeassistant/components/deconz/translations/ca.json
@@ -93,7 +93,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 00000000000..d5ec540c7a0
--- /dev/null
+++ b/homeassistant/components/deconz/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "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 fa329d2e1b3..868c5771199 100644
--- a/homeassistant/components/deconz/translations/en.json
+++ b/homeassistant/components/deconz/translations/en.json
@@ -93,7 +93,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/et.json b/homeassistant/components/deconz/translations/et.json
new file mode 100644
index 00000000000..636c75de471
--- /dev/null
+++ b/homeassistant/components/deconz/translations/et.json
@@ -0,0 +1,31 @@
+{
+ "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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json
index 4fad292f274..e873d90be17 100644
--- a/homeassistant/components/deconz/translations/fr.json
+++ b/homeassistant/components/deconz/translations/fr.json
@@ -26,6 +26,11 @@
"host": "Nom d'h\u00f4te ou adresse IP",
"port": "Port"
}
+ },
+ "user": {
+ "data": {
+ "host": "S\u00e9lectionnez la passerelle deCONZ d\u00e9couverte"
+ }
}
}
},
diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json
index 6e80c2393ac..febc22b2e01 100644
--- a/homeassistant/components/deconz/translations/lb.json
+++ b/homeassistant/components/deconz/translations/lb.json
@@ -93,7 +93,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 6eb5624f05f..f206027f1be 100644
--- a/homeassistant/components/deconz/translations/no.json
+++ b/homeassistant/components/deconz/translations/no.json
@@ -23,7 +23,8 @@
},
"manual_input": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
}
},
"user": {
@@ -47,6 +48,12 @@
"left": "Venstre",
"open": "\u00c5pen",
"right": "H\u00f8yre",
+ "side_1": "",
+ "side_2": "",
+ "side_3": "",
+ "side_4": "",
+ "side_5": "",
+ "side_6": "",
"top_buttons": "\u00d8verste knappene",
"turn_off": "Skru av",
"turn_on": "Sl\u00e5 p\u00e5"
@@ -86,7 +93,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 27dbc0535ce..2fa9549585a 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 32bf2001f44..a3120d32652 100644
--- a/homeassistant/components/deconz/translations/ru.json
+++ b/homeassistant/components/deconz/translations/ru.json
@@ -93,7 +93,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 2b941f5ae64..84b820c17cc 100644
--- a/homeassistant/components/deconz/translations/zh-Hant.json
+++ b/homeassistant/components/deconz/translations/zh-Hant.json
@@ -93,7 +93,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/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py
index 04d8e72f9a8..c4186eae505 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/translations/no.json b/homeassistant/components/demo/translations/no.json
index 3e7dfafac08..48da80fd629 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": {
@@ -16,5 +16,6 @@
}
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py
index 9f451ab3025..ed90c2ddcb0 100644
--- a/homeassistant/components/denon/media_player.py
+++ b/homeassistant/components/denon/media_player.py
@@ -111,10 +111,18 @@ class DenonDevice(MediaPlayerEntity):
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/translations/de.json b/homeassistant/components/denonavr/translations/de.json
new file mode 100644
index 00000000000..4fd05d6c578
--- /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/hu.json b/homeassistant/components/denonavr/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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/pl.json b/homeassistant/components/denonavr/translations/pl.json
index f025f6e2dd4..eb396842bf0 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/device_tracker/group.py b/homeassistant/components/device_tracker/group.py
new file mode 100644
index 00000000000..07ec2cfe985
--- /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 340c03665ff..c4f2b6f277d 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/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py
index e05350dbbac..f884ce0219d 100644
--- a/homeassistant/components/devolo_home_control/binary_sensor.py
+++ b/homeassistant/components/devolo_home_control/binary_sensor.py
@@ -67,50 +67,35 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
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}"
- else:
- 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._device_class = DEVICE_CLASS_MAPPING.get(
+ self._binary_sensor_property.sub_type
+ or self._binary_sensor_property.sensor_type
+ )
- self._subscriber = None
+ if self._device_class is None:
+ if device_instance.binary_sensor_property.get(element_uid).sub_type != "":
+ self._name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}"
+ else:
+ self._name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}"
+
+ 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 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
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 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
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 05f4363c384..297e431e63a 100644
--- a/homeassistant/components/devolo_home_control/climate.py
+++ b/homeassistant/components/devolo_home_control/climate.py
@@ -14,7 +14,7 @@ from homeassistant.const import PRECISION_HALVES
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__)
@@ -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 @@ class DevoloClimateDeviceEntity(DevoloDeviceEntity, ClimateEntity):
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 93b2cfc11e5..e596a7628f6 100644
--- a/homeassistant/components/devolo_home_control/config_flow.py
+++ b/homeassistant/components/devolo_home_control/config_flow.py
@@ -38,8 +38,8 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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)
diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py
index b93713cc700..21e555e3122 100644
--- a/homeassistant/components/devolo_home_control/cover.py
+++ b/homeassistant/components/devolo_home_control/cover.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
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 @@ class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity):
@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 @@ class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity):
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 06ddf2175f7..44f3814825e 100644
--- a/homeassistant/components/devolo_home_control/devolo_device.py
+++ b/homeassistant/components/devolo_home_control/devolo_device.py
@@ -10,14 +10,17 @@ _LOGGER = logging.getLogger(__name__)
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 @@ class DevoloDeviceEntity(Entity):
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 @@ class DevoloDeviceEntity(Entity):
"""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,19 @@ class DevoloDeviceEntity(Entity):
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 unexpected messages."""
+ if message[0].startswith("hdm"):
+ # 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 70629854dea..8056192340c 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 @@ class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity):
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 bdb3a80fff6..d7bc4804ef2 100644
--- a/homeassistant/components/devolo_home_control/manifest.json
+++ b/homeassistant/components/devolo_home_control/manifest.json
@@ -2,7 +2,7 @@
"domain": "devolo_home_control",
"name": "devolo Home Control",
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
- "requirements": ["devolo-home-control-api==0.13.0"],
+ "requirements": ["devolo-home-control-api==0.15.0"],
"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 4bb2536dcc2..bbd16dc8560 100644
--- a/homeassistant/components/devolo_home_control/sensor.py
+++ b/homeassistant/components/devolo_home_control/sensor.py
@@ -33,7 +33,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,
@@ -55,44 +55,7 @@ async def async_setup_entry(
class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity):
- """Representation of a 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._state = self._multi_level_sensor_property.value
-
- self._device_class = DEVICE_CLASS_MAPPING.get(
- self._multi_level_sensor_property.sensor_type
- )
-
- name = device_instance.item_name
-
- if self._device_class is None:
- name += f" {self._multi_level_sensor_property.sensor_type}"
-
- self._unit = self._multi_level_sensor_property.unit
-
- super().__init__(
- homecontrol=homecontrol,
- device_instance=device_instance,
- element_uid=element_uid,
- name=name,
- sync=self._sync if sync is None else sync,
- )
+ """Abstract representation of a multi level sensor within devolo Home Control."""
@property
def device_class(self) -> str:
@@ -102,24 +65,43 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity):
@property
def state(self):
"""Return the state of the sensor."""
- return self._state
+ return self._value
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit
- 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()
+
+class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity):
+ """Representation of a generic multi level sensor within devolo Home Control."""
+
+ def __init__(
+ self,
+ homecontrol,
+ device_instance,
+ element_uid,
+ ):
+ """Initialize a devolo multi level sensor."""
+ self._multi_level_sensor_property = device_instance.multi_level_sensor_property[
+ element_uid
+ ]
+
+ 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
+ )
+
+ self._value = self._multi_level_sensor_property.value
+ self._unit = self._multi_level_sensor_property.unit
+
+ if self._device_class is None:
+ self._name += f" {self._multi_level_sensor_property.sensor_type}"
class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
@@ -127,39 +109,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(
- device_instance.consumption_property[element_uid], consumption
- )
- self.sensor_type = consumption
- 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,
+ 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._unit = getattr(
+ device_instance.consumption_property[element_uid], f"{consumption}_unit"
+ )
+
+ 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/switch.py b/homeassistant/components/devolo_home_control/switch.py
index 9a7812af7bd..bc62dbfe2b9 100644
--- a/homeassistant/components/devolo_home_control/switch.py
+++ b/homeassistant/components/devolo_home_control/switch.py
@@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry
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 @@ class DevoloSwitch(SwitchEntity):
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 @@ class DevoloSwitch(SwitchEntity):
"""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 @@ class DevoloSwitch(SwitchEntity):
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 @@ class DevoloSwitch(SwitchEntity):
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/no.json b/homeassistant/components/devolo_home_control/translations/no.json
index 08b70ba7a4e..19c8d2653c1 100644
--- a/homeassistant/components/devolo_home_control/translations/no.json
+++ b/homeassistant/components/devolo_home_control/translations/no.json
@@ -13,8 +13,10 @@
"mydevolo_url": "mydevolo URL",
"password": "Passord",
"username": "E-postadresse / devolo-ID"
- }
+ },
+ "title": ""
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json
index af843097539..31ded6b7f9e 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/et.json b/homeassistant/components/dexcom/translations/et.json
new file mode 100644
index 00000000000..b1967bf2695
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/et.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "M\u00f5\u00f5t\u00fchik"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json
new file mode 100644
index 00000000000..9f2fd5d72f4
--- /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/nl.json b/homeassistant/components/dexcom/translations/nl.json
new file mode 100644
index 00000000000..4d00f0bfc74
--- /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 de99cfe0fbc..61ad015b5a4 100644
--- a/homeassistant/components/dexcom/translations/no.json
+++ b/homeassistant/components/dexcom/translations/no.json
@@ -12,6 +12,7 @@
"user": {
"data": {
"password": "Passord",
+ "server": "",
"username": "Brukernavn"
},
"description": "Angi Dexcom Share-legitimasjon",
diff --git a/homeassistant/components/dexcom/translations/pl.json b/homeassistant/components/dexcom/translations/pl.json
index 24ae7a17370..acc27dc93f4 100644
--- a/homeassistant/components/dexcom/translations/pl.json
+++ b/homeassistant/components/dexcom/translations/pl.json
@@ -1,12 +1,12 @@
{
"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",
+ "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json
new file mode 100644
index 00000000000..99298014f71
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "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."
+ },
+ "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/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py
index d076dae9210..eb1345df45c 100644
--- a/homeassistant/components/digital_ocean/binary_sensor.py
+++ b/homeassistant/components/digital_ocean/binary_sensor.py
@@ -3,7 +3,11 @@ import logging
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 @@ from . import (
_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 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
@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/directv/translations/no.json b/homeassistant/components/directv/translations/no.json
index 88f36decaea..c6db33d32d0 100644
--- a/homeassistant/components/directv/translations/no.json
+++ b/homeassistant/components/directv/translations/no.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "Tilkobling feilet"
},
+ "flow_title": "",
"step": {
"ssdp_confirm": {
"description": "Vil du sette opp {name} ?"
diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json
index bec0198ca70..db0dc7ea0a4 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/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
index 4130f67ec13..f4180ffcffa 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 @@ _LOGGER = logging.getLogger(__name__)
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 @@ class Doods(ImageProcessingEntity):
self._matches = {}
self._total_matches = 0
self._last_image = None
+ self._process_time = 0
@property
def camera_entity(self):
@@ -228,6 +231,7 @@ class Doods(ImageProcessingEntity):
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 @@ class Doods(ImageProcessingEntity):
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 @@ class Doods(ImageProcessingEntity):
_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 @@ class Doods(ImageProcessingEntity):
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 43ab0c96153..1f7e02e8569 100644
--- a/homeassistant/components/doorbird/__init__.py
+++ b/homeassistant/components/doorbird/__init__.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
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 @@ class DoorBirdRequestView(HomeAssistantView):
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 07b753da6ee..8e3f661254d 100644
--- a/homeassistant/components/doorbird/config_flow.py
+++ b/homeassistant/components/doorbird/config_flow.py
@@ -7,7 +7,13 @@ from doorbirdpy import DoorBird
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/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json
index 304760dbf58..fd8bf04d29e 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/nl.json b/homeassistant/components/doorbird/translations/nl.json
index 85180df8b4a..2bf97d687ab 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/pl.json b/homeassistant/components/doorbird/translations/pl.json
index a24febcd94a..446fd21626a 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/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json
new file mode 100644
index 00000000000..14e637f5f98
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/ca.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json
new file mode 100644
index 00000000000..e8e23bf8343
--- /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
new file mode 100644
index 00000000000..ea382532a71
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/fr.json
@@ -0,0 +1,11 @@
+{
+ "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 00000000000..3b2d79a34a7
--- /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/it.json b/homeassistant/components/dsmr/translations/it.json
new file mode 100644
index 00000000000..b295fb60747
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/it.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "one": "uno",
+ "other": "altri"
+ },
+ "step": {
+ "one": "uno",
+ "other": "altri"
+ }
+ }
+}
\ 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 00000000000..9c8fbbe80a9
--- /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/nb.json b/homeassistant/components/dsmr/translations/nb.json
new file mode 100644
index 00000000000..6ba5a1f3978
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/nb.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json
index 815a6f19706..637a81a3f87 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 0ec67bc97fd..5fda67e65a3 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 @@ DEFINITIONS = {
"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 @@ DEFINITIONS = {
"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 00000000000..44b4442dc31
--- /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/no.json b/homeassistant/components/dunehd/translations/no.json
index 6230845a09e..e395c28b7a9 100644
--- a/homeassistant/components/dunehd/translations/no.json
+++ b/homeassistant/components/dunehd/translations/no.json
@@ -13,7 +13,8 @@
"data": {
"host": "Vert"
},
- "description": "Konfigurer Dune HD-integrering. Hvis du har problemer med konfigurasjonen, kan du g\u00e5 til: https://www.home-assistant.io/integrations/dunehd \n\nKontroller at spilleren er sl\u00e5tt p\u00e5."
+ "description": "Konfigurer Dune HD-integrering. Hvis du har problemer med konfigurasjonen, kan du g\u00e5 til: https://www.home-assistant.io/integrations/dunehd \n\nKontroller at spilleren er sl\u00e5tt p\u00e5.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json
index 2e0e2b352ca..893a71d4b70 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 e67fbb08e29..df4c412cc62 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 db985e57a41..c076fc81628 100644
--- a/homeassistant/components/dweet/__init__.py
+++ b/homeassistant/components/dweet/__init__.py
@@ -6,6 +6,7 @@ import dweepy
import voluptuous as vol
from homeassistant.const import (
+ ATTR_FRIENDLY_NAME,
CONF_NAME,
CONF_WHITELIST,
EVENT_STATE_CHANGED,
@@ -58,7 +59,7 @@ def setup(hass, config):
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/fr.json b/homeassistant/components/eafm/translations/fr.json
index ca9d788e75d..2d963bbe5df 100644
--- a/homeassistant/components/eafm/translations/fr.json
+++ b/homeassistant/components/eafm/translations/fr.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"no_stations": "Aucune station de surveillance des inondations n'a \u00e9t\u00e9 trouv\u00e9e."
},
"step": {
diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..4e7bfc9dc93
--- /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 00000000000..637a81a3f87
--- /dev/null
+++ b/homeassistant/components/eafm/translations/pl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py
index 855e62727b5..00c40344d6e 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 @@ from homeassistant.const import (
)
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 @@ CONF_CIRCUIT = "circuit"
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 @@ CONFIG_SCHEMA = vol.Schema(
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 @@ class EbusdData:
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 63f72a89ccd..badb94a6f85 100644
--- a/homeassistant/components/ebusd/sensor.py
+++ b/homeassistant/components/ebusd/sensor.py
@@ -3,6 +3,7 @@ import datetime
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_BEGIN = "time_frame2_begin"
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 @@ class EbusdSensor(Entity):
"""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/sensor.py b/homeassistant/components/ecobee/sensor.py
index d9d6e74e3de..a3276c53e3b 100644
--- a/homeassistant/components/ecobee/sensor.py
+++ b/homeassistant/components/ecobee/sensor.py
@@ -108,7 +108,11 @@ class EcobeeSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
- if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, "unknown"]:
+ if self._state in [
+ ECOBEE_STATE_CALIBRATING,
+ ECOBEE_STATE_UNKNOWN,
+ "unknown",
+ ]:
return None
if self.type == "temperature":
diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py
index c33c9765730..0bc4c21fc2f 100644
--- a/homeassistant/components/edl21/sensor.py
+++ b/homeassistant/components/edl21/sensor.py
@@ -77,6 +77,18 @@ class EDL21:
# D=7: Instantaneous value
# E=0: Total
"1-0:16.7.0*255": "Sum active instantaneous power",
+ # C=36: Active power L1
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:36.7.0*255": "L1 active instantaneous power",
+ # C=56: Active power L1
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:56.7.0*255": "L2 active instantaneous power",
+ # C=76: Active power L1
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:76.7.0*255": "L3 active instantaneous power",
}
_OBIS_BLACKLIST = {
# A=129: Manufacturer specific
diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py
index 4be443a36f4..6882171b67f 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 @@ from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE
_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/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json
index 9c78b3411e3..bb7e56211de 100644
--- a/homeassistant/components/elgato/translations/no.json
+++ b/homeassistant/components/elgato/translations/no.json
@@ -11,7 +11,8 @@
"step": {
"user": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
"description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant."
},
diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json
index 263c67a67ca..7088ede092a 100644
--- a/homeassistant/components/elgato/translations/pl.json
+++ b/homeassistant/components/elgato/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 Elgato Key Light."
},
"error": {
diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py
index 1c299e68803..3bcacde64b9 100644
--- a/homeassistant/components/elkm1/alarm_control_panel.py
+++ b/homeassistant/components/elkm1/alarm_control_panel.py
@@ -155,18 +155,16 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
self.async_write_ha_state()
def _watch_area(self, area, changeset):
- if not changeset.get("log_event"):
+ last_log = changeset.get("last_log")
+ if not last_log:
+ return
+ # user_number only set for arm/disarm logs
+ if not last_log.get("user_number"):
return
self._changed_by_keypad = None
- self._changed_by_id = area.log_number
- self._changed_by = username(self._elk, area.log_number - 1)
- self._changed_by_time = "%04d-%02d-%02dT%02d:%02d" % (
- area.log_year,
- area.log_month,
- area.log_day,
- area.log_hour,
- area.log_minute,
- )
+ self._changed_by_id = last_log["user_number"]
+ self._changed_by = username(self._elk, self._changed_by_id - 1)
+ self._changed_by_time = last_log["timestamp"]
self.async_write_ha_state()
@property
diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json
index ca694157ba7..dd62c3a4989 100644
--- a/homeassistant/components/elkm1/manifest.json
+++ b/homeassistant/components/elkm1/manifest.json
@@ -2,7 +2,7 @@
"domain": "elkm1",
"name": "Elk-M1 Control",
"documentation": "https://www.home-assistant.io/integrations/elkm1",
- "requirements": ["elkm1-lib==0.7.19"],
+ "requirements": ["elkm1-lib==0.8.0"],
"codeowners": ["@gwww", "@bdraco"],
"config_flow": true
}
diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json
index 81265e587b2..618299def29 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/pl.json b/homeassistant/components/elkm1/translations/pl.json
index b38c9aaa6d2..c7f21554dee 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/emulated_roku/translations/et.json b/homeassistant/components/emulated_roku/translations/et.json
index d6a9fded4b6..cc4dd04a9ea 100644
--- a/homeassistant/components/emulated_roku/translations/et.json
+++ b/homeassistant/components/emulated_roku/translations/et.json
@@ -3,9 +3,12 @@
"step": {
"user": {
"data": {
- "name": "Nimi"
+ "host_ip": "",
+ "name": "Nimi",
+ "upnp_bind_multicast": "Seo multicast (jah/ei)"
}
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json
index 9664031c000..eb98e1fb2b9 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/enocean/translations/no.json b/homeassistant/components/enocean/translations/no.json
index b87607d2f35..a3fc35edcc8 100644
--- a/homeassistant/components/enocean/translations/no.json
+++ b/homeassistant/components/enocean/translations/no.json
@@ -22,5 +22,6 @@
"title": "Angi banen til din ENOcean dongle"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py
index 4813fd47a92..873d9935ee8 100644
--- a/homeassistant/components/envirophat/sensor.py
+++ b/homeassistant/components/envirophat/sensor.py
@@ -6,7 +6,13 @@ import logging
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 @@ SENSOR_TYPES = {
"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/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json
index 9a23a04b540..3c2dafff34d 100644
--- a/homeassistant/components/esphome/translations/no.json
+++ b/homeassistant/components/esphome/translations/no.json
@@ -23,7 +23,8 @@
},
"user": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
"description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node."
}
diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py
new file mode 100644
index 00000000000..1636054663d
--- /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 6652568a0a7..2b141351e1d 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 80b64c28c2f..3fd103cd244 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 f109103a99c..ca752208f12 100644
--- a/homeassistant/components/ffmpeg/__init__.py
+++ b/homeassistant/components/ffmpeg/__init__.py
@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONTENT_TYPE_MULTIPART,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
@@ -122,9 +123,9 @@ class FFmpegManager:
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 a8842f9c401..9b4218c011c 100644
--- a/homeassistant/components/ffmpeg_motion/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py
@@ -4,7 +4,11 @@ import logging
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 @@ class FFmpegMotion(FFmpegBinarySensor):
@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 6ada2bb2748..387f25afe6e 100644
--- a/homeassistant/components/ffmpeg_noise/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py
@@ -4,7 +4,7 @@ import logging
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 @@ class FFmpegNoise(FFmpegBinarySensor):
@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 251bd1df6a3..21ce22ea8e3 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 @@ _LOGGER = logging.getLogger(__name__)
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 e9e7265f917..3c812970022 100644
--- a/homeassistant/components/fibaro/sensor.py
+++ b/homeassistant/components/fibaro/sensor.py
@@ -7,6 +7,7 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -35,7 +36,7 @@ SENSOR_TYPES = {
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 @@ class FibaroSensor(FibaroDevice, Entity):
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/firmata/__init__.py b/homeassistant/components/firmata/__init__.py
index b64a88cbf57..c0394a95a49 100644
--- a/homeassistant/components/firmata/__init__.py
+++ b/homeassistant/components/firmata/__init__.py
@@ -6,7 +6,17 @@ import logging
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 .board import FirmataBoard
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 @@ ANALOG_PIN_SCHEMA = vol.All(cv.string, vol.Match(r"^A[0-9]+$"))
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 @@ SWITCH_SCHEMA = vol.Schema(
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 @@ BOARD_CONFIG_SCHEMA = vol.Schema(
),
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 async_setup_entry(
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 4576b8dc69e..c2708fc2753 100644
--- a/homeassistant/components/firmata/binary_sensor.py
+++ b/homeassistant/components/firmata/binary_sensor.py
@@ -4,10 +4,10 @@ import logging
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 bae30014d63..73e3c004cb9 100644
--- a/homeassistant/components/firmata/board.py
+++ b/homeassistant/components/firmata/board.py
@@ -5,17 +5,23 @@ from typing import Union
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 @@ class FirmataBoard:
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 @@ board %s: %s",
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 1ad3cbb8423..6259582b5f7 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 00000000000..e95b5101413
--- /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 d894c0a440b..8b283c4f81d 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 644986fb66c..3259d76cbb3 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 @@ class FirmataBoardPin:
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 @@ class FirmataBinaryDigitalOutput(FirmataBoardPin):
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 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
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 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
"""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 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
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 00000000000..cb9db1f11e5
--- /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/switch.py b/homeassistant/components/firmata/switch.py
index ab67a6d6840..f1aaf3357c0 100644
--- a/homeassistant/components/firmata/switch.py
+++ b/homeassistant/components/firmata/switch.py
@@ -4,16 +4,10 @@ import logging
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 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity):
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/fr.json b/homeassistant/components/firmata/translations/fr.json
new file mode 100644
index 00000000000..a66d58dce87
--- /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/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json
index b69e8de8f7c..d63283fe36b 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/nl.json b/homeassistant/components/flick_electric/translations/nl.json
index 5f7433d97db..c4901d328c3 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/pl.json b/homeassistant/components/flick_electric/translations/pl.json
index fb6554d00d8..19c319a366b 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/flo/translations/de.json b/homeassistant/components/flo/translations/de.json
new file mode 100644
index 00000000000..6f398062876
--- /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/fr.json b/homeassistant/components/flo/translations/fr.json
new file mode 100644
index 00000000000..4013a390696
--- /dev/null
+++ b/homeassistant/components/flo/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ },
+ "title": "flo"
+}
\ 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 00000000000..3b2d79a34a7
--- /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 00000000000..7235d67c278
--- /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 00000000000..4d00f0bfc74
--- /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 00000000000..25dab56796c
--- /dev/null
+++ b/homeassistant/components/flo/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/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json
index a746d793bc4..fdb7ab8ed9a 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/pl.json b/homeassistant/components/flume/translations/pl.json
index ff1d73fe0ce..f899b1446a6 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/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json
new file mode 100644
index 00000000000..aae3ef835bb
--- /dev/null
+++ b/homeassistant/components/flunearyou/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/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json
index 27789e1b4cf..50300be55e1 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/pl.json b/homeassistant/components/flunearyou/translations/pl.json
index de344b82d00..cde9f39c3f9 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/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json
index d86d47c4137..ac58ddf9639 100644
--- a/homeassistant/components/forked_daapd/translations/no.json
+++ b/homeassistant/components/forked_daapd/translations/no.json
@@ -17,7 +17,8 @@
"data": {
"host": "Vert",
"name": "Vennlig navn",
- "password": "API-passord (la st\u00e5 tomt hvis ingen passord)"
+ "password": "API-passord (la st\u00e5 tomt hvis ingen passord)",
+ "port": ""
},
"title": "Konfigurere forked-daapd-enhet"
}
diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json
index d40e9b282aa..7f17565c2cd 100644
--- a/homeassistant/components/forked_daapd/translations/pl.json
+++ b/homeassistant/components/forked_daapd/translations/pl.json
@@ -5,6 +5,7 @@
},
"error": {
"unknown_error": "Nieznany b\u0142\u0105d.",
+ "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"
},
"step": {
diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py
index bae0336a63e..6f33c9ff591 100644
--- a/homeassistant/components/foursquare/__init__.py
+++ b/homeassistant/components/foursquare/__init__.py
@@ -5,7 +5,12 @@ import requests
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 setup(hass, config):
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/strings.json b/homeassistant/components/freebox/strings.json
index 0fdc4571a1d..2257e7bd908 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"
+ "connection_failed": "[%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 264e0ed3038..d5be94be363 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",
+ "connection_failed": "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/en.json b/homeassistant/components/freebox/translations/en.json
index 15e18a8982b..a0b5c98e72f 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",
+ "connection_failed": "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/it.json b/homeassistant/components/freebox/translations/it.json
index 11d27eebd69..33f1ccbc1eb 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",
+ "connection_failed": "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/no.json b/homeassistant/components/freebox/translations/no.json
index f93450837a2..6c9d5e2e9fa 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",
+ "connection_failed": "Tilkobling mislyktes.",
"register_failed": "Registrering feilet, vennligst pr\u00f8v igjen",
- "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere"
+ "unknown": "Uventet feil"
},
"step": {
"link": {
@@ -15,8 +15,10 @@
},
"user": {
"data": {
- "host": "Vert"
- }
+ "host": "Vert",
+ "port": ""
+ },
+ "title": ""
}
}
}
diff --git a/homeassistant/components/freebox/translations/ru.json b/homeassistant/components/freebox/translations/ru.json
index 1bef863e15f..377c0e8002f 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.",
+ "connection_failed": "\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/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json
index be643ab9fd9..47ccc2e57b8 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",
+ "connection_failed": "\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 7db216c32e1..1246eb4afaf 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 @@ class FritzboxBinarySensor(BinarySensorEntity):
@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/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 73eb24d08cd..66c1b6d997f 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 d0c86f2cdf5..6732f6e99c3 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==20200918.2"],
+ "requirements": ["home-assistant-frontend==20201001.1"],
"dependencies": [
"api",
"auth",
diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml
index 31eb4d5d1ca..cc0d6bde216 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/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json
index 1c814306b3b..9058d46d02a 100644
--- a/homeassistant/components/garmin_connect/translations/no.json
+++ b/homeassistant/components/garmin_connect/translations/no.json
@@ -15,7 +15,8 @@
"password": "Passord",
"username": "Brukernavn"
},
- "description": "Fyll inn legitimasjonen din."
+ "description": "Fyll inn legitimasjonen din.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json
index 982c7b2c50b..5aaa67a913e 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/gdacs/translations/no.json b/homeassistant/components/gdacs/translations/no.json
index 3ca22c398e0..372a24c0b38 100644
--- a/homeassistant/components/gdacs/translations/no.json
+++ b/homeassistant/components/gdacs/translations/no.json
@@ -5,6 +5,9 @@
},
"step": {
"user": {
+ "data": {
+ "radius": ""
+ },
"title": "Fyll ut filterdetaljene."
}
}
diff --git a/homeassistant/components/geonetnz_quakes/translations/no.json b/homeassistant/components/geonetnz_quakes/translations/no.json
index 3ca22c398e0..fc3b339d807 100644
--- a/homeassistant/components/geonetnz_quakes/translations/no.json
+++ b/homeassistant/components/geonetnz_quakes/translations/no.json
@@ -5,6 +5,10 @@
},
"step": {
"user": {
+ "data": {
+ "mmi": "",
+ "radius": ""
+ },
"title": "Fyll ut filterdetaljene."
}
}
diff --git a/homeassistant/components/geonetnz_volcano/translations/no.json b/homeassistant/components/geonetnz_volcano/translations/no.json
index 50ffa06071e..646afcc1d16 100644
--- a/homeassistant/components/geonetnz_volcano/translations/no.json
+++ b/homeassistant/components/geonetnz_volcano/translations/no.json
@@ -5,6 +5,9 @@
},
"step": {
"user": {
+ "data": {
+ "radius": ""
+ },
"title": "Fyll inn dine filterdetaljer."
}
}
diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json
index 7df7ba57b3b..784b75c9ee5 100644
--- a/homeassistant/components/gios/translations/no.json
+++ b/homeassistant/components/gios/translations/no.json
@@ -14,7 +14,8 @@
"name": "Navn p\u00e5 integrasjon",
"station_id": "ID til m\u00e5lestasjon"
},
- "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios"
+ "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json
index 666aaa6bf00..dd593c4add6 100644
--- a/homeassistant/components/glances/translations/no.json
+++ b/homeassistant/components/glances/translations/no.json
@@ -13,6 +13,7 @@
"host": "Vert",
"name": "Navn",
"password": "Passord",
+ "port": "",
"ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet",
"username": "Brukernavn",
"verify_ssl": "Bekreft sertifiseringen av systemet",
diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py
new file mode 100644
index 00000000000..892ee46982d
--- /dev/null
+++ b/homeassistant/components/goalzero/__init__.py
@@ -0,0 +1,140 @@
+"""The Goal Zero Yeti integration."""
+import asyncio
+import logging
+
+from goalzero import Yeti, exceptions
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import (
+ DATA_KEY_API,
+ DATA_KEY_COORDINATOR,
+ DEFAULT_NAME,
+ DOMAIN,
+ MIN_TIME_BETWEEN_UPDATES,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+GOALZERO_SCHEMA = vol.Schema(
+ vol.All(
+ {
+ vol.Required(CONF_HOST): cv.matches_regex(
+ r"\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2 \
+ [0-4][0-9]|[01]?[0-9][0-9]?)\Z"
+ ),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ },
+ )
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [GOALZERO_SCHEMA]))},
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+PLATFORMS = ["binary_sensor"]
+
+
+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]
+
+ _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, 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:
+ _LOGGER.warning("Failed to update data from Yeti")
+ 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, sensor_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 00000000000..25b370c459f
--- /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, sensor_name, server_unique_id)
+
+ self._condition = sensor_name
+
+ variable_info = BINARY_SENSOR_DICT[sensor_name]
+ self._condition_name = variable_info[0]
+ self._icon = variable_info[2]
+ self.api = api
+ self._device_class = variable_info[1]
+
+ @property
+ 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
+
+ @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 00000000000..31c1a51efeb
--- /dev/null
+++ b/homeassistant/components/goalzero/config_flow.py
@@ -0,0 +1,75 @@
+"""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)
+ return self.async_create_entry(
+ title=name,
+ data={CONF_HOST: host, CONF_NAME: name},
+ )
+ except exceptions.ConnectError:
+ errors["base"] = "cannot_connect"
+ _LOGGER.exception("Error connecting to device at %s", host)
+ except exceptions.InvalidHost:
+ errors["base"] = "invalid_host"
+ _LOGGER.exception("Invalid data received from device at %s", host)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ user_input = user_input or {}
+ return self.async_show_form(
+ 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 endpoint
+
+ 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 00000000000..3afa1e537c1
--- /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 00000000000..803b8f7eaae
--- /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 00000000000..e7a134c01ec
--- /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": "Name"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_host": "This is not the Yeti you are looking for",
+ "unknown": "Unknown Error"
+ },
+ "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 00000000000..56d181f5d98
--- /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": "Aquest no \u00e9s el Yeti que est\u00e0s buscant",
+ "unknown": "Error desconegut"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "name": "Name"
+ },
+ "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 00000000000..61936c6ff56
--- /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 00000000000..98cfa4f6f33
--- /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": "This is not the Yeti you are looking for",
+ "unknown": "Unknown 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 00000000000..4897899d8c3
--- /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/fr.json b/homeassistant/components/goalzero/translations/fr.json
new file mode 100644
index 00000000000..a155e8370d1
--- /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 00000000000..2a76a68ace5
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/it.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..5972c69873b
--- /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 00000000000..eafe3e7715a
--- /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": "Dette er ikke Yetien du leter etter",
+ "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 00000000000..5a196f56b03
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/pl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..96f77605d82
--- /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": "\u042d\u0442\u043e \u043d\u0435 \u0442\u043e\u0442 Yeti, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0412\u044b \u0438\u0449\u0435\u0442\u0435.",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\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 00000000000..cde1a763b6b
--- /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": "\u4e26\u4e0d\u662f\u6240\u8981\u641c\u5c0b\u7684 Yeti",
+ "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 5c0ef55ff3f..2f6ac76122f 100644
--- a/homeassistant/components/gogogate2/const.py
+++ b/homeassistant/components/gogogate2/const.py
@@ -4,3 +4,4 @@ DOMAIN = "gogogate2"
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 8e753eb6ae5..e748b420edb 100644
--- a/homeassistant/components/gogogate2/cover.py
+++ b/homeassistant/components/gogogate2/cover.py
@@ -34,7 +34,7 @@ from .common import (
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 @@ class DeviceCover(CoordinatorEntity, CoverEntity):
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/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json
index bb5797d6517..a68c0e6384c 100644
--- a/homeassistant/components/gogogate2/translations/ca.json
+++ b/homeassistant/components/gogogate2/translations/ca.json
@@ -15,7 +15,7 @@
"username": "Nom d'usuari"
},
"description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria.",
- "title": "Configuraci\u00f3 de GogoGate2"
+ "title": "Configuraci\u00f3 de GogoGate2 o iSmartGate"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json
index 478e7e8ccf8..79f216738c4 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/it.json b/homeassistant/components/gogogate2/translations/it.json
index 378d55630a4..7b1dbe4e3e4 100644
--- a/homeassistant/components/gogogate2/translations/it.json
+++ b/homeassistant/components/gogogate2/translations/it.json
@@ -15,7 +15,7 @@
"username": "Nome utente"
},
"description": "Fornire le informazioni richieste di seguito.",
- "title": "Configurazione GogoGate2"
+ "title": "Configurazione di GogoGate2 o iSmartGate"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json
index 8adc85e0c26..b050a2eadd1 100644
--- a/homeassistant/components/gogogate2/translations/no.json
+++ b/homeassistant/components/gogogate2/translations/no.json
@@ -15,7 +15,7 @@
"username": "Brukernavn"
},
"description": "Gi n\u00f8dvendig informasjon nedenfor.",
- "title": "Konfigurer GogoGate2"
+ "title": "Sett opp GogoGate2 eller iSmartGate"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json
index 7a6c33be781..2ea10491255 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 7b75a36f8bb..4bf0df8b933 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -10,7 +10,11 @@ import jwt
# 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 @@ class GoogleConfig(AbstractConfig):
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/griddy/sensor.py b/homeassistant/components/griddy/sensor.py
index acdcefee527..7a155586fac 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 @@ class GriddyPriceSensor(CoordinatorEntity):
@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/translations/pl.json b/homeassistant/components/griddy/translations/pl.json
index e28b4b6f2e6..4fe11d213e8 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/group/__init__.py b/homeassistant/components/group/__init__.py
index 87eb2cd615b..4a0050868d9 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 @@ from homeassistant.const import (
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 @@ SERVICE_REMOVE = "remove"
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 @@ CONFIG_SCHEMA = vol.Schema(
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."""
-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
+ 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()
- return None, None
+ def exclude_domain(self) -> None:
+ """Exclude the current domain."""
+ self.exclude_domains.add(current_domain.get())
+
+ 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."""
+ if REG_KEY not in hass.data:
+ # Integration not setup yet, it cannot be on
+ return False
+
state = hass.states.get(entity_id)
- if state:
- group_on, _ = _get_group_on_off(state.state)
-
- # 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 async_setup(hass, config):
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 @@ class Group(Entity):
"""
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 @@ class Group(Entity):
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 @@ class Group(Entity):
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 @@ class Group(Entity):
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 @@ class Group(Entity):
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 @@ class Group(Entity):
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)
+ self._see_state(state)
- return states
+ 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 @@ class Group(Entity):
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
+ if tr_state:
+ self._see_state(tr_state)
- # 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 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/translations/ca.json b/homeassistant/components/group/translations/ca.json
index 21b6361589c..552a2c9677e 100644
--- a/homeassistant/components/group/translations/ca.json
+++ b/homeassistant/components/group/translations/ca.json
@@ -1,7 +1,7 @@
{
"state": {
"_": {
- "closed": "Tancat/da",
+ "closed": "Tancat/ada",
"home": "A casa",
"locked": "Bloquejat",
"not_home": "Fora",
diff --git a/homeassistant/components/group/translations/nb.json b/homeassistant/components/group/translations/nb.json
index 7d2edd69113..14ac7fac24f 100644
--- a/homeassistant/components/group/translations/nb.json
+++ b/homeassistant/components/group/translations/nb.json
@@ -6,6 +6,7 @@
"locked": "L\u00e5st",
"not_home": "Borte",
"off": "Av",
+ "ok": "",
"on": "P\u00e5",
"open": "\u00c5pen",
"problem": "Problem",
diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json
index 698af4fe68c..763021190c1 100644
--- a/homeassistant/components/group/translations/no.json
+++ b/homeassistant/components/group/translations/no.json
@@ -6,8 +6,10 @@
"locked": "L\u00e5st",
"not_home": "Borte",
"off": "Av",
+ "ok": "",
"on": "P\u00e5",
"open": "\u00c5pen",
+ "problem": "",
"unlocked": "Ul\u00e5st"
}
},
diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py
index 366596beb0b..e6ed422db0f 100644
--- a/homeassistant/components/growatt_server/sensor.py
+++ b/homeassistant/components/growatt_server/sensor.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
+ CURRENCY_EURO,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
@@ -21,6 +22,7 @@ from homeassistant.const import (
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
FREQUENCY_HERTZ,
+ PERCENTAGE,
POWER_WATT,
TEMP_CELSIUS,
VOLT,
@@ -39,8 +41,8 @@ SCAN_INTERVAL = datetime.timedelta(minutes=5)
# 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_SENSOR_TYPES = {
),
"storage_battery_percentage": (
"Battery percentage",
- "%",
+ PERCENTAGE,
"capacity",
{"device_class": DEVICE_CLASS_BATTERY},
),
@@ -338,7 +340,7 @@ STORAGE_SENSOR_TYPES = {
),
"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 c63d80163bc..7942dba361e 100644
--- a/homeassistant/components/guardian/binary_sensor.py
+++ b/homeassistant/components/guardian/binary_sensor.py
@@ -3,7 +3,11 @@ from typing import Callable, Dict
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 @@ ATTR_CONNECTED_CLIENTS = "connected_clients"
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/translations/no.json b/homeassistant/components/guardian/translations/no.json
index 850424df514..fbe5f881124 100644
--- a/homeassistant/components/guardian/translations/no.json
+++ b/homeassistant/components/guardian/translations/no.json
@@ -8,7 +8,8 @@
"step": {
"user": {
"data": {
- "ip_address": "IP adresse"
+ "ip_address": "IP adresse",
+ "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 22706a1babc..9c918c69796 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": {
diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json
index b1c29f3577b..e8293aff79f 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/pl.json b/homeassistant/components/hangouts/translations/pl.json
index 69c3020bbfb..2dd3364bd53 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/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json
index 12bbcfaca18..664a849061a 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/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 69c53225d49..9604507fcf0 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -127,18 +127,57 @@ MAP_SERVICE_API = {
@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]
- result = await hassio.get_addon_info(addon_id)
- return result["data"]
+ 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]
+ 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 be2cec5ae9c..95f861e6097 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -12,11 +12,14 @@ from aiohttp.web_exceptions import HTTPBadGateway
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 @@ NO_TIMEOUT = re.compile(
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 @@ class HassIOView(HomeAssistantView):
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 @@ class HassIOView(HomeAssistantView):
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/hassio/translations/nb.json b/homeassistant/components/hassio/translations/nb.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/hassio/translations/nb.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/hassio/translations/no.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json
index c3817d25776..4c307582281 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/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py
index 779afa10cca..c0da593a039 100644
--- a/homeassistant/components/hikvision/binary_sensor.py
+++ b/homeassistant/components/hikvision/binary_sensor.py
@@ -5,7 +5,12 @@ import logging
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 @@ DEFAULT_DELAY = 0
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(
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index 0e6fabb66aa..79c60572ba5 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -8,7 +8,7 @@ import time
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.const import (
)
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,26 +45,19 @@ CONF_ORDER = "use_include_order"
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(
- {
- 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,
- }
-)
+GLOB_TO_SQL_CHARS = {
+ 42: "%", # *
+ 46: "_", # .
+}
-CONFIG_SCHEMA = vol.Schema({DOMAIN: _FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA)
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
+ {vol.Optional(CONF_ORDER, default=False): cv.boolean}
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
SIGNIFICANT_DOMAINS = (
"climate",
@@ -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 @@ class HistoryPeriodView(HomeAssistantView):
# 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 @@ class Filters:
"""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))
+ return query.filter(self.entity_filter())
- query = query.filter(~States.domain.in_(IGNORE_DOMAINS))
+ @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
- entity_filter = self.entity_filter()
- if entity_filter is not None:
- query = query.filter(entity_filter)
+ return False
- return query
-
- 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
- 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)
+ 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))
- return entity_filter
+ excludes = []
+ if self.excluded_domains:
+ excludes.append(States.domain.in_(self.excluded_domains))
+ if 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))
+
+
+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/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py
index 27c648f554b..120148a8f81 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/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json
new file mode 100644
index 00000000000..6f398062876
--- /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/fr.json b/homeassistant/components/hlk_sw16/translations/fr.json
new file mode 100644
index 00000000000..45620fe7795
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..3b2d79a34a7
--- /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 00000000000..4d00f0bfc74
--- /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 00000000000..25dab56796c
--- /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 798fe2930a0..2c624f8b0a3 100644
--- a/homeassistant/components/home_connect/strings.json
+++ b/homeassistant/components/home_connect/strings.json
@@ -2,7 +2,7 @@
"config": {
"step": {
"pick_implementation": {
- "title": "Pick Authentication Method"
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
diff --git a/homeassistant/components/home_connect/translations/ca.json b/homeassistant/components/home_connect/translations/ca.json
index be6054f9bda..6553ce7e24d 100644
--- a/homeassistant/components/home_connect/translations/ca.json
+++ b/homeassistant/components/home_connect/translations/ca.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "El component Home Connect no est\u00e0 configurat. Mira'n la documentaci\u00f3."
+ "missing_configuration": "El component Home Connect 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."
diff --git a/homeassistant/components/home_connect/translations/en.json b/homeassistant/components/home_connect/translations/en.json
index 78310536205..24190814216 100644
--- a/homeassistant/components/home_connect/translations/en.json
+++ b/homeassistant/components/home_connect/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "The Home Connect component is not configured. Please follow the documentation."
+ "missing_configuration": "The Home Connect 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."
diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json
index 7457f7487d4..8c60c994df0 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 630960b1c91..42a0c34fe81 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 3899d3b749f..98aa955b020 100644
--- a/homeassistant/components/home_connect/translations/it.json
+++ b/homeassistant/components/home_connect/translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Il componente Home Connect non \u00e8 configurato. Si prega di seguire la documentazione."
+ "missing_configuration": "Il componente Home Connect 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."
diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json
index 973e1a0ec88..8d1f5554e7f 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 1820e1e2788..210c871a1b8 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 908f62efbc9..185ba6264e9 100644
--- a/homeassistant/components/home_connect/translations/no.json
+++ b/homeassistant/components/home_connect/translations/no.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "missing_configuration": "Home Connect-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"
diff --git a/homeassistant/components/home_connect/translations/ru.json b/homeassistant/components/home_connect/translations/ru.json
index 2ef6f7d6697..b354d91c20c 100644
--- a/homeassistant/components/home_connect/translations/ru.json
+++ b/homeassistant/components/home_connect/translations/ru.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "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."
+ "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."
},
"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/home_connect/translations/zh-Hant.json b/homeassistant/components/home_connect/translations/zh-Hant.json
index 5132dedd515..1d68766d2c7 100644
--- a/homeassistant/components/home_connect/translations/zh-Hant.json
+++ b/homeassistant/components/home_connect/translations/zh-Hant.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "missing_configuration": "Home Connect \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"
diff --git a/homeassistant/components/homeassistant/translations/nb.json b/homeassistant/components/homeassistant/translations/nb.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/nb.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/no.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/pt.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py
index a7377ffe43e..915856951d2 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_ENTITY_ID = "entity_id"
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,
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 2fb61a61fed..6dc2e2364b6 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -9,7 +9,11 @@ from pyhap.accessory_driver import AccessoryDriver
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 @@ from homeassistant.const import (
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 6a3206ac41b..3d35b685271 100644
--- a/homeassistant/components/homekit/config_flow.py
+++ b/homeassistant/components/homekit/config_flow.py
@@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 d8eec057191..9a2bc37a5a9 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -136,6 +136,7 @@ SERV_TELEVISION_SPEAKER = "TelevisionSpeaker"
SERV_TEMPERATURE_SENSOR = "TemperatureSensor"
SERV_THERMOSTAT = "Thermostat"
SERV_VALVE = "Valve"
+SERV_WINDOW = "Window"
SERV_WINDOW_COVERING = "WindowCovering"
# #### Characteristics ####
diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json
index 87ef0dc5ec8..486e9f1643c 100644
--- a/homeassistant/components/homekit/manifest.json
+++ b/homeassistant/components/homekit/manifest.json
@@ -20,5 +20,6 @@
"codeowners": [
"@bdraco"
],
+ "zeroconf": ["_homekit._tcp.local."],
"config_flow": true
}
diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json
index 3c6162e378e..4bac36a8467 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 1e18ad82b94..d8d8da5a974 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 @@ from .const import (
HK_POSITION_GOING_TO_MIN,
HK_POSITION_STOPPED,
SERV_GARAGE_DOOR_OPENER,
+ SERV_WINDOW,
SERV_WINDOW_COVERING,
)
@@ -128,16 +133,16 @@ class GarageDoorOpener(HomeAccessory):
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 @@ class WindowCoveringBase(HomeAccessory):
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 @@ class WindowCoveringBase(HomeAccessory):
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 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
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_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index 7d8dcac046d..feae1b5cd06 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 @@ HASS_TO_HOMEKIT = {
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 @@ class SecuritySystem(HomeAccessory):
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/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index beddd915fc0..6a9fb126c21 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
from .config_flow import normalize_hkid
from .connection import HKDevice
-from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES
+from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS
from .storage import EntityMapStorage
_LOGGER = logging.getLogger(__name__)
@@ -200,6 +200,7 @@ async def async_setup(hass, config):
zeroconf_instance = await zeroconf.async_get_instance(hass)
hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance)
hass.data[KNOWN_DEVICES] = {}
+ hass.data[TRIGGERS] = {}
return True
diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py
index 939c6055e10..29fddf99189 100644
--- a/homeassistant/components/homekit_controller/binary_sensor.py
+++ b/homeassistant/components/homekit_controller/binary_sensor.py
@@ -4,6 +4,7 @@ import logging
from aiohomekit.model.characteristics import CharacteristicsTypes
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_GAS,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
@@ -72,6 +73,24 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
return self.service.value(CharacteristicsTypes.SMOKE_DETECTED) == 1
+class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
+ """Representation of a Homekit BO sensor."""
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of this sensor."""
+ return DEVICE_CLASS_GAS
+
+ def get_characteristic_types(self):
+ """Define the homekit characteristics the entity is tracking."""
+ return [CharacteristicsTypes.CARBON_MONOXIDE_DETECTED]
+
+ @property
+ def is_on(self):
+ """Return true if CO is currently detected."""
+ return self.service.value(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED) == 1
+
+
class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit occupancy sensor."""
@@ -112,6 +131,7 @@ ENTITY_TYPES = {
"motion": HomeKitMotionSensor,
"contact": HomeKitContactSensor,
"smoke": HomeKitSmokeSensor,
+ "carbon-monoxide": HomeKitCarbonMonoxideSensor,
"occupancy": HomeKitOccupancySensor,
"leak": HomeKitLeakSensor,
}
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 9ca247382c7..9881ef15dcb 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -23,11 +23,29 @@ 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()
@@ -49,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())
@@ -66,6 +87,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
"""Initialize the homekit_controller flow."""
self.model = None
self.hkid = None
+ self.name = None
self.devices = {}
self.controller = None
self.finish_pairing = None
@@ -83,9 +105,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
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:
@@ -222,7 +246,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
# 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
@@ -235,6 +258,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
if await self._hkid_is_homekit_bridge(hkid):
return self.async_abort(reason="ignored_model")
+ self.name = name
self.model = model
self.hkid = hkid
@@ -355,9 +379,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
@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/connection.py b/homeassistant/components/homekit_controller/connection.py
index 6a59f98f3dc..ed2aaaa4656 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -16,6 +16,7 @@ from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
from .const import CONTROLLER, DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH
+from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
RETRY_INTERVAL = 60 # seconds
@@ -237,6 +238,9 @@ class HKDevice:
await self.async_create_devices()
+ # Load any triggers for this config entry
+ await async_setup_triggers_for_entry(self.hass, self.config_entry)
+
self.add_entities()
if self.watchable_characteristics:
@@ -377,6 +381,9 @@ class HKDevice:
"""Process events from accessory into HA state."""
self.available = True
+ # Process any stateless events (via device_triggers)
+ async_fire_triggers(self, new_values_dict)
+
for (aid, cid), value in new_values_dict.items():
accessory = self.current_state.setdefault(aid, {})
accessory[cid] = value
diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py
index 394750c0688..b1e32417137 100644
--- a/homeassistant/components/homekit_controller/const.py
+++ b/homeassistant/components/homekit_controller/const.py
@@ -4,6 +4,7 @@ DOMAIN = "homekit_controller"
KNOWN_DEVICES = f"{DOMAIN}-devices"
CONTROLLER = f"{DOMAIN}-controller"
ENTITY_MAP = f"{DOMAIN}-entity-map"
+TRIGGERS = f"{DOMAIN}-triggers"
HOMEKIT_DIR = ".homekit"
PAIRING_FILE = "pairing.json"
@@ -28,6 +29,7 @@ HOMEKIT_ACCESSORY_DISPATCH = {
"temperature": "sensor",
"battery": "sensor",
"smoke": "binary_sensor",
+ "carbon-monoxide": "binary_sensor",
"leak": "binary_sensor",
"fan": "fan",
"fanv2": "fan",
diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py
new file mode 100644
index 00000000000..76b82eec597
--- /dev/null
+++ b/homeassistant/components/homekit_controller/device_trigger.py
@@ -0,0 +1,268 @@
+"""Provides device automations for homekit devices."""
+from typing import List
+
+from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.characteristics.const import InputEventValues
+from aiohomekit.model.services import ServicesTypes
+from aiohomekit.utils import clamp_enum_to_char
+import voluptuous as vol
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
+from homeassistant.helpers.typing import ConfigType
+
+from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
+
+TRIGGER_TYPES = {
+ "button1",
+ "button2",
+ "button3",
+ "button4",
+ "button5",
+ "button6",
+ "button7",
+ "button8",
+ "button9",
+ "button10",
+}
+TRIGGER_SUBTYPES = {"single_press", "double_press", "long_press"}
+
+CONF_IID = "iid"
+CONF_SUBTYPE = "subtype"
+
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES),
+ }
+)
+
+HK_TO_HA_INPUT_EVENT_VALUES = {
+ InputEventValues.SINGLE_PRESS: "single_press",
+ InputEventValues.DOUBLE_PRESS: "double_press",
+ InputEventValues.LONG_PRESS: "long_press",
+}
+
+
+class TriggerSource:
+ """Represents a stateless source of event data from HomeKit."""
+
+ def __init__(self, connection, aid, triggers):
+ """Initialize a set of triggers for a device."""
+ self._hass = connection.hass
+ self._connection = connection
+ self._aid = aid
+ self._triggers = {}
+ for trigger in triggers:
+ self._triggers[(trigger["type"], trigger["subtype"])] = trigger
+ self._callbacks = {}
+
+ def fire(self, iid, value):
+ """Process events that have been received from a HomeKit accessory."""
+ for event_handler in self._callbacks.get(iid, []):
+ event_handler(value)
+
+ def async_get_triggers(self):
+ """List device triggers for homekit devices."""
+ yield from self._triggers
+
+ async def async_attach_trigger(
+ self,
+ config: TRIGGER_SCHEMA,
+ action: AutomationActionType,
+ automation_info: dict,
+ ) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+
+ def event_handler(char):
+ if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
+ return
+ self._hass.async_create_task(action({"trigger": config}))
+
+ trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
+ iid = trigger["characteristic"]
+
+ self._connection.add_watchable_characteristics([(self._aid, iid)])
+ self._callbacks.setdefault(iid, []).append(event_handler)
+
+ def async_remove_handler():
+ if iid in self._callbacks:
+ self._callbacks[iid].remove(event_handler)
+
+ return async_remove_handler
+
+
+def enumerate_stateless_switch(service):
+ """Enumerate a stateless switch, like a single button."""
+
+ # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group
+ # And is handled separately
+ if service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX):
+ if len(service.linked) > 0:
+ return []
+
+ char = service[CharacteristicsTypes.INPUT_EVENT]
+
+ # HomeKit itself supports single, double and long presses. But the
+ # manufacturer might not - clamp options to what they say.
+ all_values = clamp_enum_to_char(InputEventValues, char)
+
+ results = []
+ for event_type in all_values:
+ results.append(
+ {
+ "characteristic": char.iid,
+ "value": event_type,
+ "type": "button1",
+ "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
+ }
+ )
+ return results
+
+
+def enumerate_stateless_switch_group(service):
+ """Enumerate a group of stateless switches, like a remote control."""
+ switches = list(
+ service.accessory.services.filter(
+ service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH,
+ child_service=service,
+ order_by=[CharacteristicsTypes.SERVICE_LABEL_INDEX],
+ )
+ )
+
+ results = []
+ for idx, switch in enumerate(switches):
+ char = switch[CharacteristicsTypes.INPUT_EVENT]
+
+ # HomeKit itself supports single, double and long presses. But the
+ # manufacturer might not - clamp options to what they say.
+ all_values = clamp_enum_to_char(InputEventValues, char)
+
+ for event_type in all_values:
+ results.append(
+ {
+ "characteristic": char.iid,
+ "value": event_type,
+ "type": f"button{idx + 1}",
+ "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
+ }
+ )
+ return results
+
+
+def enumerate_doorbell(service):
+ """Enumerate doorbell buttons."""
+ input_event = service[CharacteristicsTypes.INPUT_EVENT]
+
+ # HomeKit itself supports single, double and long presses. But the
+ # manufacturer might not - clamp options to what they say.
+ all_values = clamp_enum_to_char(InputEventValues, input_event)
+
+ results = []
+ for event_type in all_values:
+ results.append(
+ {
+ "characteristic": input_event.iid,
+ "value": event_type,
+ "type": "doorbell",
+ "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
+ }
+ )
+ return results
+
+
+TRIGGER_FINDERS = {
+ "service-label": enumerate_stateless_switch_group,
+ "stateless-programmable-switch": enumerate_stateless_switch,
+ "doorbell": enumerate_doorbell,
+}
+
+
+async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
+ """Triggers aren't entities as they have no state, but we still need to set them up for a config entry."""
+ hkid = config_entry.data["AccessoryPairingID"]
+ conn = hass.data[KNOWN_DEVICES][hkid]
+
+ @callback
+ def async_add_service(aid, service_dict):
+ service_type = service_dict["stype"]
+
+ # If not a known service type then we can't handle any stateless events for it
+ if service_type not in TRIGGER_FINDERS:
+ return False
+
+ # We can't have multiple trigger sources for the same device id
+ # Can't have a doorbell and a remote control in the same accessory
+ # They have to be different accessories (they can be on the same bridge)
+ # In practice, this is inline with what iOS actually supports AFAWCT.
+ device_id = conn.devices[aid]
+ if device_id in hass.data[TRIGGERS]:
+ return False
+
+ # At the moment add_listener calls us with the raw service dict, rather than
+ # a service model. So turn it into a service ourselves.
+ accessory = conn.entity_map.aid(aid)
+ service = accessory.services.iid(service_dict["iid"])
+
+ # Just because we recognise the service type doesn't mean we can actually
+ # extract any triggers - so only proceed if we can
+ triggers = TRIGGER_FINDERS[service_type](service)
+ if len(triggers) == 0:
+ return False
+
+ trigger = TriggerSource(conn, aid, triggers)
+ hass.data[TRIGGERS][device_id] = trigger
+
+ return True
+
+ conn.add_listener(async_add_service)
+
+
+def async_fire_triggers(conn, events):
+ """Process events generated by a HomeKit accessory into automation triggers."""
+ for (aid, iid), ev in events.items():
+ if aid in conn.devices:
+ device_id = conn.devices[aid]
+ if device_id in conn.hass.data[TRIGGERS]:
+ source = conn.hass.data[TRIGGERS][device_id]
+ source.fire(iid, ev)
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device triggers for homekit devices."""
+
+ if device_id not in hass.data.get(TRIGGERS, {}):
+ return []
+
+ device = hass.data[TRIGGERS][device_id]
+
+ triggers = []
+
+ for trigger, subtype in device.async_get_triggers():
+ triggers.append(
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_TYPE: trigger,
+ CONF_SUBTYPE: subtype,
+ }
+ )
+
+ return triggers
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ config = TRIGGER_SCHEMA(config)
+
+ device_id = config[CONF_DEVICE_ID]
+ device = hass.data[TRIGGERS][device_id]
+ return await device.async_attach_trigger(config, action, automation_info)
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index 944729d2e5c..2075eb9dcc3 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -7,6 +7,7 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
)
@@ -19,8 +20,6 @@ TEMP_C_ICON = "mdi:thermometer"
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 @@ class HomeKitLightSensor(HomeKitEntity):
@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 e685a46e144..62fb51709bc 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"
}
@@ -47,5 +47,25 @@
"invalid_properties": "Invalid properties announced by device.",
"already_in_progress": "Config flow for device is already in progress."
}
+ },
+ "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"
+ }
}
}
diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json
index bf46d584917..0f05710a778 100644
--- a/homeassistant/components/homekit_controller/translations/ca.json
+++ b/homeassistant/components/homekit_controller/translations/ca.json
@@ -7,6 +7,7 @@
"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 b10fb6efe45..6ee03a37f88 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 00000000000..55f468c0fe8
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/el.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ },
+ "trigger_type": {
+ "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 544c86391be..ac4b3a13251 100644
--- a/homeassistant/components/homekit_controller/translations/en.json
+++ b/homeassistant/components/homekit_controller/translations/en.json
@@ -7,6 +7,7 @@
"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,10 +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"
}
}
},
+ "device_automation": {
+ "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 8eb450e6558..b1daa4e50cc 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 00000000000..31788215005
--- /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 1e5671a67af..9634fb784f7 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 f07fd6df8b0..1867d51d7df 100644
--- a/homeassistant/components/homekit_controller/translations/it.json
+++ b/homeassistant/components/homekit_controller/translations/it.json
@@ -7,6 +7,7 @@
"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 55c5ee0053d..a70a6269bb6 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 5431b3b30d1..7aef753e0dd 100644
--- a/homeassistant/components/homekit_controller/translations/lb.json
+++ b/homeassistant/components/homekit_controller/translations/lb.json
@@ -53,5 +53,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 20013168c81..84f5495cca7 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 45444d3d9d0..86e247a1572 100644
--- a/homeassistant/components/homekit_controller/translations/no.json
+++ b/homeassistant/components/homekit_controller/translations/no.json
@@ -7,6 +7,7 @@
"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 94e3422338b..b8dc1b190ec 100644
--- a/homeassistant/components/homekit_controller/translations/pl.json
+++ b/homeassistant/components/homekit_controller/translations/pl.json
@@ -27,6 +27,9 @@
"description": "Wprowad\u017a kod parowania HomeKit, aby u\u017cy\u0107 tego akcesorium",
"title": "Sparuj z akcesorium HomeKit"
},
+ "protocol_error": {
+ "description": "Urz\u0105dzenie mo\u017ce nie by\u0107 w trybie parowania i wymaga\u0107 "
+ },
"user": {
"data": {
"device": "Urz\u0105dzenie"
@@ -36,5 +39,20 @@
}
}
},
+ "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"
+ }
+ },
"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 fb9612ef981..2c631b99c6b 100644
--- a/homeassistant/components/homekit_controller/translations/ru.json
+++ b/homeassistant/components/homekit_controller/translations/ru.json
@@ -7,6 +7,7 @@
"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 0c57e09b09b..e57d61dcdb6 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 51f521d5c35..69c14bc8b00 100644
--- a/homeassistant/components/homekit_controller/translations/zh-Hant.json
+++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json
@@ -7,6 +7,7 @@
"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/sensor.py b/homeassistant/components/homematic/sensor.py
index 51a88bd1207..e6439c451c1 100644
--- a/homeassistant/components/homematic/sensor.py
+++ b/homeassistant/components/homematic/sensor.py
@@ -9,8 +9,11 @@ from homeassistant.const import (
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 @@ HM_UNIT_HA_CAST = {
"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/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index 50e8360675b..440dc31788f 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))
@@ -136,6 +136,44 @@ async def async_setup_entry(
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."""
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index 32191cde20e..082d7e9e355 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -30,6 +30,8 @@ from homeassistant.const import (
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, hap.home, "Duty Cycle")
@property
def device_info(self) -> Dict[str, Any]:
@@ -134,7 +136,7 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity):
return {
"identifiers": {
# Serial numbers of Homematic IP device
- (HMIPC_DOMAIN, self._device.id)
+ (HMIPC_DOMAIN, self._home.id)
}
}
@@ -281,7 +283,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity):
@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]:
@@ -367,7 +369,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity):
@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/translations/ca.json b/homeassistant/components/homematicip_cloud/translations/ca.json
index fe64ea49e83..871af68e360 100644
--- a/homeassistant/components/homematicip_cloud/translations/ca.json
+++ b/homeassistant/components/homematicip_cloud/translations/ca.json
@@ -7,7 +7,7 @@
},
"error": {
"invalid_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.",
- "invalid_sgtin_or_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.",
+ "invalid_sgtin_or_pin": "Codi PIN o SGTIN inv\u00e0lid, torna-ho a provar.",
"press_the_button": "Si us plau, prem el bot\u00f3 blau.",
"register_failed": "Error al registrar, torna-ho a provar.",
"timeout_button": "El temps d'espera m\u00e0xim per pr\u00e9mer el bot\u00f3 blau s'ha esgotat, torna-ho a provar."
diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json
index b5877fe09ce..cd300d4e4b3 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 92f07d401e6..4f97508c045 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 585334b3118..0c5f54d588a 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 9be01273fc1..4e7bfd1108c 100644
--- a/homeassistant/components/homematicip_cloud/translations/it.json
+++ b/homeassistant/components/homematicip_cloud/translations/it.json
@@ -6,7 +6,8 @@
"unknown": "Si \u00e8 verificato un errore sconosciuto."
},
"error": {
- "invalid_sgtin_or_pin": "PIN non valido, riprova.",
+ "invalid_pin": "PIN non valido, riprova.",
+ "invalid_sgtin_or_pin": "SGTIN o PIN non valido, riprovare.",
"press_the_button": "Si prega di premere il pulsante blu.",
"register_failed": "Registrazione fallita, si prega di riprovare.",
"timeout_button": "Timeout della pressione del pulsante blu, riprovare."
diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json
index b85b8ac00b1..962faa68066 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 80892a3e282..92487c12ea6 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/nb.json b/homeassistant/components/homematicip_cloud/translations/nb.json
new file mode 100644
index 00000000000..b9c78635b91
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/translations/nb.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "Ugyldig PIN-kode, pr\u00f8v p\u00e5 nytt."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json
index 7127b5c5aae..f16e385c3a0 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 cfd8e96c2a2..c317bbddd26 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/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json
index 645ba242561..f8a69ab709d 100644
--- a/homeassistant/components/homematicip_cloud/translations/pt.json
+++ b/homeassistant/components/homematicip_cloud/translations/pt.json
@@ -6,6 +6,7 @@
"unknown": "Ocorreu um erro desconhecido."
},
"error": {
+ "invalid_pin": "PIN inv\u00e1lido, por favor, tente novamente.",
"invalid_sgtin_or_pin": "PIN inv\u00e1lido. Por favor, tente novamente.",
"press_the_button": "Por favor, pressione o bot\u00e3o azul.",
"register_failed": "Falha ao registar. Por favor, tente novamente.",
diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index 29db6026cc4..c0db281d768 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -63,6 +63,7 @@ from .const import (
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,
@@ -243,6 +244,10 @@ class Router:
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
diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py
index 575cc9789ca..2b55245719b 100644
--- a/homeassistant/components/huawei_lte/binary_sensor.py
+++ b/homeassistant/components/huawei_lte/binary_sensor.py
@@ -13,7 +13,12 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_URL
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__)
@@ -29,6 +34,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)
@@ -194,3 +202,32 @@ class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor):
@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):
+ """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):
+ """Return WiFi status sensor icon."""
+ return "mdi:email-alert" if self.is_on else "mdi:email-off"
diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py
index 2c5a3f8a9f6..039bab10fb9 100644
--- a/homeassistant/components/huawei_lte/const.py
+++ b/homeassistant/components/huawei_lte/const.py
@@ -27,6 +27,7 @@ KEY_DEVICE_BASIC_INFORMATION = "device_basic_information"
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_SMS_SMS_COUNT = "sms_sms_count"
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 54e8f318cf6..80578fce7d9 100644
--- a/homeassistant/components/huawei_lte/device_tracker.py
+++ b/homeassistant/components/huawei_lte/device_tracker.py
@@ -153,12 +153,3 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
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 91cc8864eb0..375ced911c8 100644
--- a/homeassistant/components/huawei_lte/notify.py
+++ b/homeassistant/components/huawei_lte/notify.py
@@ -19,10 +19,6 @@ _LOGGER = logging.getLogger(__name__)
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 f547dbd2eb6..591506df652 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, NamedTuple, Optional, Pattern, Tuple, Union
import attr
@@ -10,13 +10,21 @@ from homeassistant.components.sensor import (
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.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 +37,66 @@ from .const import (
_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 +109,7 @@ SENSOR_META = {
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 +122,7 @@ SENSOR_META = {
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 +135,7 @@ SENSOR_META = {
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 +148,7 @@ SENSOR_META = {
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 +160,7 @@ SENSOR_META = {
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 +172,90 @@ SENSOR_META = {
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 +270,52 @@ SENSOR_META = {
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",
),
}
@@ -217,15 +331,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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,7 +368,7 @@ 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)
@@ -271,7 +385,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
@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:
@@ -285,17 +399,17 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
@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):
"""Return sensor's unit of measurement."""
- return self.meta.get("unit", self._unit)
+ return self.meta.unit or self._unit
@property
def icon(self):
"""Return icon for sensor."""
- icon = self.meta.get("icon")
+ icon = self.meta.icon
if callable(icon):
return icon(self.state)
return icon
@@ -303,7 +417,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
@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):
"""Update state."""
@@ -315,16 +429,8 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
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/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json
index 15fd57a3d33..a0fa9914c47 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/fr.json b/homeassistant/components/huawei_lte/translations/fr.json
index 5270168d9ca..2c7437d9a14 100644
--- a/homeassistant/components/huawei_lte/translations/fr.json
+++ b/homeassistant/components/huawei_lte/translations/fr.json
@@ -21,6 +21,7 @@
"user": {
"data": {
"password": "Mot de passe",
+ "url": "URL",
"username": "Nom d'utilisateur"
},
"description": "Entrez les d\u00e9tails d'acc\u00e8s au p\u00e9riph\u00e9rique. La sp\u00e9cification du nom d'utilisateur et du mot de passe est facultative, mais permet de prendre en charge davantage de fonctionnalit\u00e9s d'int\u00e9gration. En revanche, l\u2019utilisation d\u2019une connexion autoris\u00e9e peut entra\u00eener des probl\u00e8mes d\u2019acc\u00e8s \u00e0 l\u2019interface Web du p\u00e9riph\u00e9rique depuis l\u2019assistant externe lorsque l\u2019int\u00e9gration est active et inversement.",
diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json
index 1cc80845ea6..710a318d966 100644
--- a/homeassistant/components/huawei_lte/translations/no.json
+++ b/homeassistant/components/huawei_lte/translations/no.json
@@ -16,6 +16,7 @@
"response_error": "Ukjent feil fra enheten",
"unknown_connection_error": "Ukjent feil under tilkobling til enhet"
},
+ "flow_title": "",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json
index e38188d134f..405ffdf0343 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/pt.json b/homeassistant/components/huawei_lte/translations/pt.json
index f43cf3acc4f..b92752d7d13 100644
--- a/homeassistant/components/huawei_lte/translations/pt.json
+++ b/homeassistant/components/huawei_lte/translations/pt.json
@@ -15,6 +15,7 @@
"user": {
"data": {
"password": "Palavra-passe",
+ "url": "",
"username": "Nome do utilizador"
},
"title": "Configurar o Huawei LTE"
diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py
index e96f844a5e1..f5911bbb50c 100644
--- a/homeassistant/components/hue/sensor.py
+++ b/homeassistant/components/hue/sensor.py
@@ -10,6 +10,7 @@ from homeassistant.const import (
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/translations/de.json b/homeassistant/components/hue/translations/de.json
index c9c8c96f4d5..0defb33ae5e 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/et.json b/homeassistant/components/hue/translations/et.json
index e7ff3c415fb..0aeea7286d9 100644
--- a/homeassistant/components/hue/translations/et.json
+++ b/homeassistant/components/hue/translations/et.json
@@ -1,7 +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 99e82f1a89b..f19c5ec7a34 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/no.json b/homeassistant/components/hue/translations/no.json
index dbef1a20d48..2d51ee26452 100644
--- a/homeassistant/components/hue/translations/no.json
+++ b/homeassistant/components/hue/translations/no.json
@@ -22,7 +22,8 @@
"title": "Velg Hue Bridge"
},
"link": {
- "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n "
+ "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ",
+ "title": ""
},
"manual": {
"data": {
diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json
index 02dad0c3e52..6e2623d23ac 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/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py
index a6194994a9c..6bccd375207 100644
--- a/homeassistant/components/humidifier/device_action.py
+++ b/homeassistant/components/humidifier/device_action.py
@@ -6,6 +6,7 @@ import voluptuous as vol
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 7f37fc3b1fa..714a51ab016 100644
--- a/homeassistant/components/humidifier/device_condition.py
+++ b/homeassistant/components/humidifier/device_condition.py
@@ -6,6 +6,7 @@ import voluptuous as vol
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 00000000000..1636054663d
--- /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 00000000000..303edb781b6
--- /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 cd4b723a986..236c3b93343 100644
--- a/homeassistant/components/humidifier/translations/fr.json
+++ b/homeassistant/components/humidifier/translations/fr.json
@@ -6,6 +6,16 @@
"toggle": "Inverser {nom_entit\u00e9}",
"turn_off": "\u00c9teindre {entity_name}",
"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"
}
},
"state": {
diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json
new file mode 100644
index 00000000000..4081c4e13fc
--- /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/humidifier/translations/zh-Hant.json b/homeassistant/components/humidifier/translations/zh-Hant.json
index 3e37ba38f64..0534d45d705 100644
--- a/homeassistant/components/humidifier/translations/zh-Hant.json
+++ b/homeassistant/components/humidifier/translations/zh-Hant.json
@@ -9,8 +9,8 @@
},
"condition_type": {
"is_mode": "{entity_name}\u8a2d\u5b9a\u70ba\u6307\u5b9a\u6a21\u5f0f",
- "is_off": "{entity_name}\u5df2\u95dc\u9589",
- "is_on": "{entity_name}\u5df2\u958b\u555f"
+ "is_off": "{entity_name}\u70ba\u95dc\u9589",
+ "is_on": "{entity_name}j\u70ba\u958b\u555f"
},
"trigger_type": {
"target_humidity_changed": "{entity_name}\u8a2d\u5b9a\u6fd5\u5ea6\u5df2\u8b8a\u66f4",
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json
index a1bd06078c6..e5208ebdd68 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 cad41869ced..87d7b8a915e 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 853ed9460c8..e003b25ea85 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 homeassistant.helpers import aiohttp_client
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 00000000000..7d19fcc8fdf
--- /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/translations/de.json b/homeassistant/components/hvv_departures/translations/de.json
new file mode 100644
index 00000000000..b383e57bd93
--- /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/fr.json b/homeassistant/components/hvv_departures/translations/fr.json
index afc67b1087d..e6560da4047 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/nl.json b/homeassistant/components/hvv_departures/translations/nl.json
new file mode 100644
index 00000000000..4d00f0bfc74
--- /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/pl.json b/homeassistant/components/hvv_departures/translations/pl.json
index 5bf87fc08a8..7ea22e48d54 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/hyperion/light.py b/homeassistant/components/hyperion/light.py
index d1baec315bf..db34a21dada 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 @@ from homeassistant.components.light import (
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_PRIORITY = "priority"
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 device.setup():
- add_entities([device])
+ if not await hyperion_client.async_client_connect():
+ raise PlatformNotReady
+
+ 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 @@ class Hyperion(LightEntity):
@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 @@ class Hyperion(LightEntity):
@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
- brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
-
- 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},
+ # == 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)
+ )
}
- )
- self._icon = "mdi:lava-lamp"
- self._rgb_color = [175, 0, 255]
- 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}
- )
-
- 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]}
- )
-
- 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
- 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"
+
+ # == Set an external source
+ if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
+
+ # 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,
+ }
+ }
+ ):
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
+ # == 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
+
+ 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
+
+ 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
+
+ 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:
- # 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
+ self._icon = ICON_EFFECT
- 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 _update_components(self, _=None):
+ """Update Hyperion components."""
+ self.async_write_ha_state()
- 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)
+ 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()
- try:
- sock.connect((self._host, self._port))
- except OSError:
- sock.close()
- return False
+ 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()
- 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
+ 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()
- # 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
- else:
- try:
- more = sock.recv(4096)
- except socket.timeout:
- more = None
- if not more:
- buffering = False
- response = str(buf, "utf-8")
- else:
- buf += more
+ def _update_full_state(self):
+ """Update full Hyperion state."""
+ self._update_adjustment()
+ self._update_priorities()
+ self._update_effect_list()
- sock.close()
- return json.loads(response)
+ _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 6d9d0ae4d9d..4a9bf2ada8c 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/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json
new file mode 100644
index 00000000000..6425b216b9e
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "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/image/manifest.json b/homeassistant/components/image/manifest.json
index 4fc9c2d1d05..246ea387140 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/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py
index db49e119235..45d3a4f5a25 100644
--- a/homeassistant/components/influxdb/__init__.py
+++ b/homeassistant/components/influxdb/__init__.py
@@ -57,6 +57,7 @@ from .const import (
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 get_influx_connection(conf, test_write=False, test_read=False):
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 c1b5ce3a591..029e4d482e8 100644
--- a/homeassistant/components/influxdb/const.py
+++ b/homeassistant/components/influxdb/const.py
@@ -29,6 +29,7 @@ CONF_COMPONENT_CONFIG_GLOB = "component_config_glob"
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 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = {
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/input_boolean/translations/et.json b/homeassistant/components/input_boolean/translations/et.json
index 3edfbf3cb5d..e0b2d46b168 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 b0a608a1754..f08c1e111de 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 e72e7b10288..83acbf89262 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 e9a36c0fc88..716ca6fbbc0 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 f4182fbb3d5..a241a89ff53 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 cc918fabb2f..3988fe3eace 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 22378cd35a0..5cce5f5ce0b 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 c5802730c42..a87771349a8 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 047874d6328..42d7d57f419 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 bf41f9dc43c..9c1141de543 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/translations/ca.json b/homeassistant/components/insteon/translations/ca.json
index e1d470b784c..ea5300200c2 100644
--- a/homeassistant/components/insteon/translations/ca.json
+++ b/homeassistant/components/insteon/translations/ca.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"already_configured": "Ja hi ha una connexi\u00f3 amb un m\u00f2dem Insteon configurada",
- "cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
- "cannot_connect": "No s'ha pogut connectar al m\u00f2dem Insteon, torna-ho a provar.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"select_single": "Selecciona una opci\u00f3."
},
"step": {
@@ -57,7 +57,7 @@
},
"plm": {
"data": {
- "device": "Dispositiu PLM (ex: /dev/ttyUSB0 o COM3)"
+ "device": "Ruta del port USB del dispositiu"
},
"description": "Configura el m\u00f2dem Insteon PowerLink (PLM).",
"title": "Insteon PLM"
@@ -77,7 +77,7 @@
"cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon"
},
"error": {
- "cannot_connect": "No s'ha pogut connectar al m\u00f2dem Insteon, torna-ho a provar.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"input_error": "Entrades inv\u00e0lides, revisa els valors.",
"select_single": "Selecciona una opci\u00f3."
},
@@ -103,10 +103,10 @@
},
"change_hub_config": {
"data": {
- "host": "Nou nom d'amfitri\u00f3 o adre\u00e7a IP",
- "password": "Nova contrasenya",
- "port": "Nou n\u00famero de port",
- "username": "Nou nom d'usuari"
+ "host": "Adre\u00e7a IP",
+ "password": "Contrasenya",
+ "port": "Port",
+ "username": "Nom d'usuari"
},
"description": "Canvia la informaci\u00f3 de connexi\u00f3 de l'Insteon Hub. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del Hub en si. Per canviar la configuraci\u00f3 del Hub, utilitza la seva aplicaci\u00f3.",
"title": "Insteon"
diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json
new file mode 100644
index 00000000000..dfefff8c559
--- /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/fr.json b/homeassistant/components/insteon/translations/fr.json
index e0f56f093af..f18df011048 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,22 +73,30 @@
},
"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": {
- "platform": "Plate-forme"
+ "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)"
},
"description": "Modifiez le mot de passe Insteon Hub.",
"title": "Insteon"
@@ -95,12 +108,15 @@
"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": {
"data": {
+ "add_override": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.",
"add_x10": "Ajouter un appareil X10.",
"change_hub_config": "Modifier la configuration du Hub.",
+ "remove_override": "Supprimer un remplacement d'appareil",
"remove_x10": "Retirez un p\u00e9riph\u00e9rique X10."
},
"description": "S\u00e9lectionnez une option \u00e0 configurer.",
@@ -110,6 +126,7 @@
"data": {
"address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer"
},
+ "description": "Supprimer un remplacement d'appareil",
"title": "Insteon"
},
"remove_x10": {
diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json
new file mode 100644
index 00000000000..7c77bd49e27
--- /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 bff6ff757c1..367b626a6d0 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,12 @@
},
"description": "Insteon PowerLink Modem (PLM) konfigur\u00e9ieren.",
"title": "Insteon PLM"
+ },
+ "user": {
+ "data": {
+ "modem_type": "Typ vu Modem."
+ },
+ "title": "Insteon"
}
}
},
diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json
new file mode 100644
index 00000000000..538755dd013
--- /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 cb697462ca9..baf2397d00e 100644
--- a/homeassistant/components/insteon/translations/pl.json
+++ b/homeassistant/components/insteon/translations/pl.json
@@ -1,13 +1,19 @@
{
"config": {
"abort": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
+ "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"
},
"step": {
+ "hub2": {
+ "data": {
+ "password": "[%key_id:common::config_flow::data::password%]",
+ "username": "[%key_id:common::config_flow::data::username%]"
+ }
+ },
"hubv1": {
"data": {
"host": "Adres IP",
@@ -34,7 +40,7 @@
},
"options": {
"error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"change_hub_config": {
diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json
index 4e44844483d..e25fe7db5bd 100644
--- a/homeassistant/components/insteon/translations/pt.json
+++ b/homeassistant/components/insteon/translations/pt.json
@@ -42,9 +42,15 @@
},
"options": {
"error": {
- "cannot_connect": "Falha na liga\u00e7\u00e3o"
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "select_single": "Selecione uma op\u00e7\u00e3o."
},
"step": {
+ "add_x10": {
+ "data": {
+ "platform": "Plataforma"
+ }
+ },
"change_hub_config": {
"data": {
"host": "Endere\u00e7o IP",
@@ -52,6 +58,12 @@
"port": "Porta",
"username": "Nome de Utilizador"
}
+ },
+ "init": {
+ "data": {
+ "remove_x10": "Remova um dispositivo X10."
+ },
+ "description": "Selecione uma op\u00e7\u00e3o para configurar."
}
}
}
diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py
index 62dd72973da..f9c682bf527 100644
--- a/homeassistant/components/ios/notify.py
+++ b/homeassistant/components/ios/notify.py
@@ -12,6 +12,7 @@ from homeassistant.components.notify import (
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 @@ class iOSNotificationService(BaseNotificationService):
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/ipma/translations/et.json b/homeassistant/components/ipma/translations/et.json
new file mode 100644
index 00000000000..32fab1be8df
--- /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/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json
new file mode 100644
index 00000000000..44e9fc8218f
--- /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 eba\u00f5nnestus kuna \u00fchenduse uuendamine on vajalik.",
+ "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/no.json b/homeassistant/components/ipp/translations/no.json
index 749b5e5bab8..4e94efe71c1 100644
--- a/homeassistant/components/ipp/translations/no.json
+++ b/homeassistant/components/ipp/translations/no.json
@@ -19,6 +19,7 @@
"data": {
"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"
},
diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json
index 4e5af33041c..c5096f6af8e 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/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index 1f862bb1bbf..5ab331df44e 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -3,6 +3,6 @@
"name": "IQVIA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
- "requirements": ["numpy==1.19.1", "pyiqvia==0.2.1"],
+ "requirements": ["numpy==1.19.2", "pyiqvia==0.2.1"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json
index c10a1da59f2..22f45ac2f0e 100644
--- a/homeassistant/components/iqvia/translations/fr.json
+++ b/homeassistant/components/iqvia/translations/fr.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ce code postal a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9."
+ },
"error": {
"invalid_zip_code": "Code postal invalide"
},
diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json
index f3dd4f82b62..f6a914bd07d 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/no.json b/homeassistant/components/iqvia/translations/no.json
index 9359014dcf1..98526dfed24 100644
--- a/homeassistant/components/iqvia/translations/no.json
+++ b/homeassistant/components/iqvia/translations/no.json
@@ -11,7 +11,8 @@
"data": {
"zip_code": "Postnummer"
},
- "description": "Fyll ut ditt amerikanske eller kanadiske postnummer."
+ "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json
index 857ce4c2dff..b57c338ecea 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."
+ "one_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 6b01d442df8..d9f0801bc66 100644
--- a/homeassistant/components/islamic_prayer_times/translations/ca.json
+++ b/homeassistant/components/islamic_prayer_times/translations/ca.json
@@ -1,7 +1,7 @@
{
"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."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/en.json b/homeassistant/components/islamic_prayer_times/translations/en.json
index e9781c17fb1..9e8125ed141 100644
--- a/homeassistant/components/islamic_prayer_times/translations/en.json
+++ b/homeassistant/components/islamic_prayer_times/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Only a single instance is necessary."
+ "one_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"user": {
diff --git a/homeassistant/components/islamic_prayer_times/translations/no.json b/homeassistant/components/islamic_prayer_times/translations/no.json
index 59e601648ff..66398ddd473 100644
--- a/homeassistant/components/islamic_prayer_times/translations/no.json
+++ b/homeassistant/components/islamic_prayer_times/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig."
+ "one_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 66f2e918f65..252eb042416 100644
--- a/homeassistant/components/islamic_prayer_times/translations/ru.json
+++ b/homeassistant/components/islamic_prayer_times/translations/ru.json
@@ -1,7 +1,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."
+ "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."
},
"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 b9dc6928e01..f03cd67c650 100644
--- a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json
+++ b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"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"
},
"step": {
"user": {
diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py
index d911fae2c82..e003a52c91f 100644
--- a/homeassistant/components/isy994/const.py
+++ b/homeassistant/components/isy994/const.py
@@ -44,7 +44,10 @@ from homeassistant.components.lock import DOMAIN as LOCK
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 @@ from homeassistant.const import (
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 @@ from homeassistant.const import (
TIME_YEARS,
UV_INDEX,
VOLT,
+ VOLUME_CUBIC_FEET,
+ VOLUME_CUBIC_METERS,
VOLUME_GALLONS,
VOLUME_LITERS,
)
@@ -316,9 +325,9 @@ UOM_FRIENDLY_NAME = {
"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 @@ UOM_FRIENDLY_NAME = {
"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 @@ UOM_FRIENDLY_NAME = {
"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_FRIENDLY_NAME = {
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 @@ UOM_FRIENDLY_NAME = {
"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/translations/de.json b/homeassistant/components/isy994/translations/de.json
index b9c3362c488..d14dfa6c65a 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 102d1fabf05..5afaf2ad830 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 a39fc58dc25..b252dde3257 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 27f79ef2801..d35b9e91eb6 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/juicenet/translations/pl.json b/homeassistant/components/juicenet/translations/pl.json
index 601ce0c9128..c308ad2524a 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/knx/__init__.py b/homeassistant/components/knx/__init__.py
index 5a2f29e6247..87ade4955a0 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -6,7 +6,12 @@ from xknx import XKNX
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 @@ from homeassistant.helpers import discovery
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_ROUTING = "routing"
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 @@ CONFIG_SCHEMA = vol.Schema(
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 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema(
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 @@ class KNXModule:
"""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 @@ class KNXModule:
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 @@ class KNXModule:
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 @@ class KNXModule:
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 @@ class KNXModule:
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 f3b7e881134..a62e95f1def 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 b5aaeb67907..1960627a8d6 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -14,8 +14,8 @@ from homeassistant.components.climate.const import (
)
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 @@ PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items())
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 @@ class KNXClimate(ClimateEntity):
"""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 @@ class KNXClimate(ClimateEntity):
@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 @@ class KNXClimate(ClimateEntity):
"""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 @@ class KNXClimate(ClimateEntity):
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 @@ class KNXClimate(ClimateEntity):
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 @@ class KNXClimate(ClimateEntity):
"""
_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 a81fc526415..8b0dd90393b 100644
--- a/homeassistant/components/knx/const.py
+++ b/homeassistant/components/knx/const.py
@@ -15,7 +15,6 @@ from homeassistant.components.climate.const import (
)
DOMAIN = "knx"
-DATA_KNX = "data_knx"
CONF_STATE_ADDRESS = "state_address"
CONF_SYNC_STATE = "sync_state"
@@ -60,3 +59,5 @@ PRESET_MODES = {
"Standby": PRESET_AWAY,
"Comfort": PRESET_COMFORT,
}
+
+ATTR_COUNTER = "counter"
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 8c50bb2afe9..c677b12c0ee 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -15,65 +15,39 @@ from homeassistant.components.cover import (
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 @@ class KNXCover(CoverEntity):
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 @@ class KNXCover(CoverEntity):
"""
# 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 @@ class KNXCover(CoverEntity):
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 42c4dd675f5..3334e49ce38 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 xknx.devices import (
)
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 @@ from .schema import (
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 00000000000..296bcb2f540
--- /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 6d8438df0f9..d9f0f9c0d3a 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -12,10 +12,10 @@ from homeassistant.components.light import (
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 @@ DEFAULT_WHITE_VALUE = 255
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 @@ class KNXLight(LightEntity):
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 @@ class KNXLight(LightEntity):
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 @@ class KNXLight(LightEntity):
@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 @@ class KNXLight(LightEntity):
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 @@ class KNXLight(LightEntity):
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 8986d85b8b6..2d387f0653d 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 e47cfca2794..7210795bd71 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -5,13 +5,13 @@ from xknx.devices import Notification as XknxNotification
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 b4df94a0fd4..6c76fdbd199 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -5,30 +5,26 @@ from xknx.devices import Scene as XknxScene
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 a436f2dcdc8..84a54536db5 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 d87119239cf..fc2cbced8bb 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 c378d1b0ca4..ae3048e2d23 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 97500ef8194..097fa661f4a 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 @@ class KNXWeather(WeatherEntity):
"""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 c7df170b5c9..c174cf28406 100644
--- a/homeassistant/components/kodi/browse_media.py
+++ b/homeassistant/components/kodi/browse_media.py
@@ -5,6 +5,7 @@ from homeassistant.components.media_player import BrowseError, BrowseMedia
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 @@ from homeassistant.components.media_player.const import (
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 @@ CHILD_TYPE_MEDIA_CLASS = {
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,
}
@@ -147,6 +150,15 @@ async def build_item_response(media_library, payload):
season["seasondetails"].get("thumbnail")
)
title = season["seasondetails"]["label"]
+ elif search_type == MEDIA_TYPE_CHANNEL:
+ media = await media_library._server.PVR.GetChannels(
+ {
+ "channelgroupid": "alltv",
+ "properties": ["thumbnail", "channeltype", "channel", "broadcastnow"],
+ }
+ )
+ media = media.get("channels")
+ title = "Channels"
if media is None:
return None
@@ -227,9 +239,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 +295,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/manifest.json b/homeassistant/components/kodi/manifest.json
index da4daf85ada..a9df9718f8c 100644
--- a/homeassistant/components/kodi/manifest.json
+++ b/homeassistant/components/kodi/manifest.json
@@ -4,7 +4,8 @@
"documentation": "https://www.home-assistant.io/integrations/kodi",
"requirements": ["pykodi==0.2.0"],
"codeowners": [
- "@OnFreund"
+ "@OnFreund",
+ "@cgtobi"
],
"zeroconf": ["_xbmc-jsonrpc-h._tcp.local."],
"config_flow": true
diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json
new file mode 100644
index 00000000000..ac98f2016b4
--- /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/et.json b/homeassistant/components/kodi/translations/et.json
new file mode 100644
index 00000000000..9d7c8e2a028
--- /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 00000000000..3b2d79a34a7
--- /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/ko.json b/homeassistant/components/kodi/translations/ko.json
new file mode 100644
index 00000000000..6dc6b8bf87a
--- /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 872615f5bea..adbae8fafe0 100644
--- a/homeassistant/components/kodi/translations/lb.json
+++ b/homeassistant/components/kodi/translations/lb.json
@@ -46,5 +46,11 @@
}
}
}
+ },
+ "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 00000000000..235d5a50be6
--- /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/pl.json b/homeassistant/components/kodi/translations/pl.json
index 3e71fd0df8b..28dc89056f3 100644
--- a/homeassistant/components/kodi/translations/pl.json
+++ b/homeassistant/components/kodi/translations/pl.json
@@ -1,15 +1,15 @@
{
"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"
},
"step": {
"credentials": {
diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json
index 12aef9a997d..f28cee08d1b 100644
--- a/homeassistant/components/kodi/translations/pt.json
+++ b/homeassistant/components/kodi/translations/pt.json
@@ -18,16 +18,21 @@
"username": "Nome de Utilizador"
}
},
+ "discovery_confirm": {
+ "title": "Kodi descoberto"
+ },
"host": {
"data": {
"host": "Servidor",
- "port": "Porta"
+ "port": "Porta",
+ "ssl": "Conecte-se por SSL"
}
},
"user": {
"data": {
"host": "Servidor",
- "port": "Porta"
+ "port": "Porta",
+ "ssl": "Conecte-se por SSL"
},
"description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os."
},
diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json
index be48b5e15c2..3c020967c8d 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/nl.json b/homeassistant/components/konnected/translations/nl.json
index dcb5f1ed6c4..8e9eba0d134 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 ab7bae93b13..af39b9750a4 100644
--- a/homeassistant/components/konnected/translations/no.json
+++ b/homeassistant/components/konnected/translations/no.json
@@ -20,7 +20,8 @@
},
"user": {
"data": {
- "host": "IP adresse"
+ "host": "IP adresse",
+ "port": ""
},
"description": "Vennligst skriv inn verten informasjon for din Konnected Panel."
}
diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json
index 1f9b60bbae3..95b3a9f5d5e 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/lcn/manifest.json b/homeassistant/components/lcn/manifest.json
index 74bfe105555..6eeb4a69b26 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/pl.json b/homeassistant/components/life360/translations/pl.json
index 19a6c6d8828..b0a5785a320 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_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py
index 75ab4656794..cf39d70d89a 100644
--- a/homeassistant/components/lifx_cloud/scene.py
+++ b/homeassistant/components/lifx_cloud/scene.py
@@ -9,7 +9,13 @@ import async_timeout
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 00000000000..1636054663d
--- /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 4eef7a85267..f137faa0bf7 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 06c880fff77..67685889c54 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/light/translations/zh-Hant.json b/homeassistant/components/light/translations/zh-Hant.json
index 0872a742f6f..65c57839464 100644
--- a/homeassistant/components/light/translations/zh-Hant.json
+++ b/homeassistant/components/light/translations/zh-Hant.json
@@ -9,8 +9,8 @@
"turn_on": "\u958b\u555f{entity_name}"
},
"condition_type": {
- "is_off": "{entity_name}\u5df2\u95dc\u9589",
- "is_on": "{entity_name}\u5df2\u958b\u555f"
+ "is_off": "{entity_name}\u70ba\u95dc\u9589",
+ "is_on": "{entity_name}j\u70ba\u958b\u555f"
},
"trigger_type": {
"turned_off": "{entity_name}\u5df2\u95dc\u9589",
diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py
index c4a14210d32..bb81a022891 100644
--- a/homeassistant/components/linode/binary_sensor.py
+++ b/homeassistant/components/linode/binary_sensor.py
@@ -3,7 +3,11 @@ import logging
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 @@ from . import (
_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 @@ class LinodeBinarySensor(BinarySensorEntity):
@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/translations/et.json b/homeassistant/components/local_ip/translations/et.json
new file mode 100644
index 00000000000..70493aee468
--- /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/lock/group.py b/homeassistant/components/lock/group.py
new file mode 100644
index 00000000000..d64b2172750
--- /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 448d1d4531a..1ebf7e1e6dd 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 0c7786de90b..9eb3c80435c 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -7,25 +7,24 @@ import re
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 @@ from homeassistant.const import (
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 @@ import homeassistant.util.dt as dt_util
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 @@ GROUP_BY_MINUTES = 15
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 @@ HOMEASSISTANT_EVENTS = [
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 @@ class LogbookView(HomeAssistantView):
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 @@ class LogbookView(HomeAssistantView):
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,
+ 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
)
- .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))
- )
- .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))
- )
- else:
- query = query.filter(
- (States.last_updated == States.last_changed)
- | (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)
- if apply_sql_entities_filter and filters:
- entity_filter = filters.entity_filter()
- if entity_filter is not None:
- query = query.filter(
- entity_filter | (Events.event_type != EVENT_STATE_CHANGED)
+ query = query.union_all(
+ _generate_states_query(
+ session, start_day, end_day, old_state, entity_ids
)
+ )
+ else:
+ 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)
+ | (Events.event_type != EVENT_STATE_CHANGED)
+ )
+ if filters:
+ query = query.filter(
+ 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}."
+ domain = event.data_domain
- return entities_filter is None or entities_filter(entity_id)
+ if domain is None:
+ return False
-
-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}"
+ 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 @@ class LazyEventPartialState:
"""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 @@ class LazyEventPartialState:
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 @@ class LazyEventPartialState:
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/lovelace/translations/nb.json b/homeassistant/components/lovelace/translations/nb.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/nb.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/no.json b/homeassistant/components/lovelace/translations/no.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/no.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json
index d18cbae5103..3b51aab6e4a 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 9d184969139..91e9c96d429 100644
--- a/homeassistant/components/luftdaten/__init__.py
+++ b/homeassistant/components/luftdaten/__init__.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_SENSORS,
CONF_SHOW_ON_MAP,
PERCENTAGE,
+ PRESSURE_PA,
TEMP_CELSIUS,
)
from homeassistant.core import callback
@@ -44,8 +45,8 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update"
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/luftdaten/translations/no.json b/homeassistant/components/luftdaten/translations/no.json
index 841ba4ad3da..8c1b69bed07 100644
--- a/homeassistant/components/luftdaten/translations/no.json
+++ b/homeassistant/components/luftdaten/translations/no.json
@@ -10,7 +10,8 @@
"data": {
"show_on_map": "Vis p\u00e5 kart",
"station_id": "Luftdaten Sensor ID"
- }
+ },
+ "title": ""
}
}
}
diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json
index 02c48b586f2..0674172e975 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/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py
index b42c96f99c2..1897f3b7465 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,7 +33,7 @@ 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
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 62d53b17c47..1dbb642aee9 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 41c1a4e8690..1bf0e213a25 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -27,6 +27,7 @@ from homeassistant.const import (
HTTP_INTERNAL_SERVER_ERROR,
HTTP_NOT_FOUND,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
@@ -882,7 +883,7 @@ class MediaPlayerImageView(HomeAssistantView):
"""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 = (
@@ -891,7 +892,7 @@ class MediaPlayerImageView(HomeAssistantView):
)
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/group.py b/homeassistant/components/media_player/group.py
new file mode 100644
index 00000000000..b612165fa19
--- /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 a90e4fffdc1..64955d1913b 100644
--- a/homeassistant/components/media_player/reproduce_state.py
+++ b/homeassistant/components/media_player/reproduce_state.py
@@ -5,7 +5,6 @@ from typing import Any, Dict, Iterable, Optional
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 @@ from .const import (
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 _async_reproduce_states(
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 _async_reproduce_states(
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 _async_reproduce_states(
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/ca.json b/homeassistant/components/media_player/translations/ca.json
index 1a1c161e5ec..67f7aad655b 100644
--- a/homeassistant/components/media_player/translations/ca.json
+++ b/homeassistant/components/media_player/translations/ca.json
@@ -13,7 +13,7 @@
"idle": "Inactiu",
"off": "OFF",
"on": "ON",
- "paused": "Pausat/da",
+ "paused": "Pausat/ada",
"playing": "Reproduint",
"standby": "En espera"
}
diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json
index 2800870e9cc..4d71a30a8ac 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/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py
index ed6fc31c414..8813883c151 100644
--- a/homeassistant/components/melcloud/config_flow.py
+++ b/homeassistant/components/melcloud/config_flow.py
@@ -9,7 +9,13 @@ import pymelcloud
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 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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/translations/pl.json b/homeassistant/components/melcloud/translations/pl.json
index 44467601826..cd0c961089e 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/met/translations/et.json b/homeassistant/components/met/translations/et.json
new file mode 100644
index 00000000000..3d66c2a532e
--- /dev/null
+++ b/homeassistant/components/met/translations/et.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "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/no.json b/homeassistant/components/met/translations/no.json
index 39c8336e074..90489288b62 100644
--- a/homeassistant/components/met/translations/no.json
+++ b/homeassistant/components/met/translations/no.json
@@ -11,6 +11,7 @@
"longitude": "Lengdegrad",
"name": "Navn"
},
+ "description": "",
"title": "Lokasjon"
}
}
diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json
index d134c28020f..1afabe51e79 100644
--- a/homeassistant/components/met/translations/pt.json
+++ b/homeassistant/components/met/translations/pt.json
@@ -11,6 +11,7 @@
"longitude": "Longitude",
"name": "Nome"
},
+ "description": "",
"title": "Localiza\u00e7\u00e3o"
}
}
diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
index 8b4d3a33501..59524ed1a80 100644
--- a/homeassistant/components/meteo_france/const.py
+++ b/homeassistant/components/meteo_france/const.py
@@ -4,6 +4,7 @@ from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
@@ -108,7 +109,7 @@ SENSOR_TYPES = {
},
"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/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json
index 166ddaa68ab..4b8dc3204dd 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 10c5915fdbc..91eea1fcec7 100644
--- a/homeassistant/components/meteo_france/translations/no.json
+++ b/homeassistant/components/meteo_france/translations/no.json
@@ -19,7 +19,8 @@
"data": {
"city": "By"
},
- "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn"
+ "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn",
+ "title": ""
}
}
},
diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json
index 46f3c1fdc27..8ff70e51de5 100644
--- a/homeassistant/components/meteo_france/translations/pl.json
+++ b/homeassistant/components/meteo_france/translations/pl.json
@@ -5,6 +5,12 @@
"unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej."
},
"step": {
+ "cities": {
+ "data": {
+ "city": "Miasto"
+ },
+ "description": "Wybierz swoje miasto z listy"
+ },
"user": {
"data": {
"city": "Miasto"
diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json
index 3137ef26505..025d58f5197 100644
--- a/homeassistant/components/meteo_france/translations/pt.json
+++ b/homeassistant/components/meteo_france/translations/pt.json
@@ -4,7 +4,8 @@
"user": {
"data": {
"city": "Cidade"
- }
+ },
+ "title": ""
}
}
}
diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py
index b481b417b9e..6b13d03ebba 100644
--- a/homeassistant/components/meteoalarm/binary_sensor.py
+++ b/homeassistant/components/meteoalarm/binary_sensor.py
@@ -5,7 +5,11 @@ import logging
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_COUNTRY = "country"
CONF_LANGUAGE = "language"
CONF_PROVINCE = "province"
-DEFAULT_DEVICE_CLASS = "safety"
DEFAULT_NAME = "meteoalarm"
SCAN_INTERVAL = timedelta(minutes=30)
@@ -78,7 +81,7 @@ class MeteoAlertBinarySensor(BinarySensorEntity):
@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/translations/de.json b/homeassistant/components/metoffice/translations/de.json
new file mode 100644
index 00000000000..55896dc4901
--- /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 00000000000..c6ad082c40e
--- /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/fr.json b/homeassistant/components/metoffice/translations/fr.json
index 44d4762d547..a046a71fe95 100644
--- a/homeassistant/components/metoffice/translations/fr.json
+++ b/homeassistant/components/metoffice/translations/fr.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
"error": {
"cannot_connect": "Echec de connexion",
"unknown": "Erreur inattendue"
diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 7167faf5494..6b129f2965c 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/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py
index 208cecf6d3b..69a738724c3 100644
--- a/homeassistant/components/microsoft_face/__init__.py
+++ b/homeassistant/components/microsoft_face/__init__.py
@@ -8,7 +8,7 @@ from aiohttp.hdrs import CONTENT_TYPE
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 @@ class MicrosoftFace:
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 6206c67dc03..94db0417ddb 100644
--- a/homeassistant/components/miflora/sensor.py
+++ b/homeassistant/components/miflora/sensor.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -53,7 +54,7 @@ ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update"
# 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"],
diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json
index 0049c69632f..31fad4b081c 100644
--- a/homeassistant/components/mikrotik/translations/fr.json
+++ b/homeassistant/components/mikrotik/translations/fr.json
@@ -27,6 +27,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/no.json b/homeassistant/components/mikrotik/translations/no.json
index 0ae25436433..1e528fa4986 100644
--- a/homeassistant/components/mikrotik/translations/no.json
+++ b/homeassistant/components/mikrotik/translations/no.json
@@ -14,6 +14,7 @@
"host": "Vert",
"name": "Navn",
"password": "Passord",
+ "port": "",
"username": "Brukernavn",
"verify_ssl": "Bruk ssl"
},
diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json
new file mode 100644
index 00000000000..9fa7c48bb68
--- /dev/null
+++ b/homeassistant/components/mill/translations/fr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ },
+ "error": {
+ "connection_error": "\u00c9chec de connexion"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json
new file mode 100644
index 00000000000..4d00f0bfc74
--- /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 c9bef09227c..eaf1da95e9e 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/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py
index b2cb3d22e4b..7c5cbd135ed 100644
--- a/homeassistant/components/mobile_app/helpers.py
+++ b/homeassistant/components/mobile_app/helpers.py
@@ -7,7 +7,7 @@ from aiohttp.web import Response, json_response
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 62bb5fdf08d..04d308a5a05 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -12,7 +12,12 @@ from homeassistant.components.notify import (
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 @@ class MobileAppNotificationService(BaseNotificationService):
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 @@ class MobileAppNotificationService(BaseNotificationService):
" 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 00000000000..e5c01546976
--- /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 2f5e69fd02b..bbeea2e9521 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -26,6 +26,7 @@ from homeassistant.const import (
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 0a7ea08543a..822000cb56a 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -6,29 +6,50 @@ from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpC
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 @@ from .const import (
_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 @@ SERIAL_SCHEMA = BASE_SCHEMA.extend(
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 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend(
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 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
}
)
+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 c12c50cdc07..dc29dd626ae 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -46,6 +46,7 @@ ATTR_UNIT = "unit"
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_UNIT = "temperature_unit"
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 00000000000..a7c9c301ac5
--- /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 a9155c7b628..05e9c39c4b5 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/monoprice/strings.json b/homeassistant/components/monoprice/strings.json
index c25fb901d76..008c182f41b 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 6af5204b91e..1e4c623c215 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 9e9f3a4d2cf..08438f8a985 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 b89758a9da3..d084929e320 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 45954d9840b..93efecbf54a 100644
--- a/homeassistant/components/monoprice/translations/no.json
+++ b/homeassistant/components/monoprice/translations/no.json
@@ -4,12 +4,13 @@
"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": {
"user": {
"data": {
+ "port": "",
"source_1": "Navn p\u00e5 kilden #1",
"source_2": "Navn p\u00e5 kilden #2",
"source_3": "Navn p\u00e5 kilden #3",
diff --git a/homeassistant/components/monoprice/translations/pl.json b/homeassistant/components/monoprice/translations/pl.json
index b5af0e8851f..020e0f2c554 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 5b891db80a3..4fb5eb892a1 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 ca1923d62d6..e653bda9205 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.et.json b/homeassistant/components/moon/translations/sensor.et.json
new file mode 100644
index 00000000000..e1477727868
--- /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/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index bafbead96d6..74a664532df 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -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 @@ class MQTT:
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 @@ class MQTT:
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)
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index a7d5236148b..6c4cbfd212f 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -32,6 +32,7 @@ SUPPORTED_COMPONENTS = [
"lock",
"sensor",
"switch",
+ "tag",
"vacuum",
]
@@ -154,6 +155,12 @@ async def async_start(
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 8b293eb06f6..4d44090a4e3 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 75c3fdec260..d2d18af6e60 100644
--- a/homeassistant/components/mqtt/strings.json
+++ b/homeassistant/components/mqtt/strings.json
@@ -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",
@@ -82,4 +82,4 @@
"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 00000000000..94356ccf778
--- /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 e94be2d1a6d..7709c784b9f 100644
--- a/homeassistant/components/mqtt/translations/ca.json
+++ b/homeassistant/components/mqtt/translations/ca.json
@@ -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 7256fe2f956..7f34e10fa89 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 8ece91cb85d..7ccdf153c13 100644
--- a/homeassistant/components/mqtt/translations/en.json
+++ b/homeassistant/components/mqtt/translations/en.json
@@ -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 00000000000..e8a9fac81d7
--- /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/nl.json b/homeassistant/components/mqtt/translations/nl.json
index 7953c744f27..2b8119aca4d 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 c5ee982b63d..5f91b68b9b8 100644
--- a/homeassistant/components/mqtt/translations/no.json
+++ b/homeassistant/components/mqtt/translations/no.json
@@ -12,6 +12,7 @@
"broker": "Megler",
"discovery": "Aktiver oppdagelse",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler."
@@ -58,6 +59,7 @@
"data": {
"broker": "Megler",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler."
@@ -70,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/ru.json b/homeassistant/components/mqtt/translations/ru.json
index 4ff21126cfa..d6540e702e4 100644
--- a/homeassistant/components/mqtt/translations/ru.json
+++ b/homeassistant/components/mqtt/translations/ru.json
@@ -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 02978a22327..0483ad35cd3 100644
--- a/homeassistant/components/mqtt/translations/zh-Hant.json
+++ b/homeassistant/components/mqtt/translations/zh-Hant.json
@@ -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/translations/pl.json b/homeassistant/components/myq/translations/pl.json
index aefc8336903..d01f9e588fa 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/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py
index 0bab6ea6eea..4ec3c6e0abd 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 @@ from homeassistant.const import STATE_ON
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 8ff2139a7b4..6a6e95ddd01 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -9,12 +9,14 @@ from homeassistant.const import (
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 @@ SENSORS = {
"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/translations/et.json b/homeassistant/components/neato/translations/et.json
new file mode 100644
index 00000000000..6a45426e3d0
--- /dev/null
+++ b/homeassistant/components/neato/translations/et.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "unexpected_error": "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/pl.json b/homeassistant/components/neato/translations/pl.json
index 80e0a1df48e..821cf79c971 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/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py
index dd52e1d665f..0e9198a0220 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 @@ CLIMATE_BINARY_TYPES = {
}
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 @@ class NestActivityZoneSensor(NestBinarySensor):
@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 8b0af5011ec..df03c054398 100644
--- a/homeassistant/components/nest/local_auth.py
+++ b/homeassistant/components/nest/local_auth.py
@@ -4,6 +4,7 @@ from functools import partial
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/climate.py b/homeassistant/components/netatmo/climate.py
index f24591fe954..30ce38753c6 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -315,7 +315,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
@property
def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation if supported."""
- if self._model == NA_THERM:
+ if self._model == NA_THERM and self._boilerstatus is not None:
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
# Maybe it is a valve
if self._room_status and self._room_status.get("heating_power_request", 0) > 0:
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index 2368d54efdf..af41e01c7df 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_MBAR,
SPEED_KILOMETERS_PER_HOUR,
@@ -52,9 +53,9 @@ SENSOR_TYPES = {
"pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE],
"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 last 24h", 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],
diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json
index 99104c168cf..f1c91932c7e 100644
--- a/homeassistant/components/netatmo/translations/ca.json
+++ b/homeassistant/components/netatmo/translations/ca.json
@@ -3,6 +3,7 @@
"abort": {
"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": "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": {
diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json
index 04ac8e69f11..e31d801b7a0 100644
--- a/homeassistant/components/netatmo/translations/en.json
+++ b/homeassistant/components/netatmo/translations/en.json
@@ -3,6 +3,7 @@
"abort": {
"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": {
diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json
index a72728e8438..1ad29ab080d 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 00000000000..cf0944dbe0e
--- /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 fd4072dc54a..fe8fc74d273 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/it.json b/homeassistant/components/netatmo/translations/it.json
index a9e54b31bd7..1b98b7d01bc 100644
--- a/homeassistant/components/netatmo/translations/it.json
+++ b/homeassistant/components/netatmo/translations/it.json
@@ -3,6 +3,7 @@
"abort": {
"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": {
diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json
index f8c052bd5f8..8165941f0d8 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 f46b8627e0c..7dad13d085a 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": {
diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json
index 5cc1d400719..98e3206f46a 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 8a549e4cd30..721c3a2a946 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/ru.json b/homeassistant/components/netatmo/translations/ru.json
index 69a0430a4ba..c9be7e60825 100644
--- a/homeassistant/components/netatmo/translations/ru.json
+++ b/homeassistant/components/netatmo/translations/ru.json
@@ -3,6 +3,7 @@
"abort": {
"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": "\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": {
diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json
index fd528ed4cfc..588675c670e 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/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json
index 84844ddbd94..7f09569418d 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/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py
index 88939cbe790..1b67963bcc3 100644
--- a/homeassistant/components/nightscout/__init__.py
+++ b/homeassistant/components/nightscout/__init__.py
@@ -7,7 +7,7 @@ from aiohttp import ClientError
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 bd33bc8dcb4..3000d652e46 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 b3e9b3a0d55..ecc44258e90 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 a6e100ae8f2..2240bcec02b 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 eb06d94de3b..21a472680b4 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 00000000000..a7ad0fe1d27
--- /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 00000000000..2a4ee09725b
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/el.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API"
+ },
+ "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 b7947c84997..d8b4c441283 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 9a03055bac4..307b8ede5aa 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/fr.json b/homeassistant/components/nightscout/translations/fr.json
index 5b31b06ea3a..1bcd530d29b 100644
--- a/homeassistant/components/nightscout/translations/fr.json
+++ b/homeassistant/components/nightscout/translations/fr.json
@@ -3,9 +3,16 @@
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
+ "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 00000000000..3b2d79a34a7
--- /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 e0305d1b5e2..d9c90233a3a 100644
--- a/homeassistant/components/nightscout/translations/it.json
+++ b/homeassistant/components/nightscout/translations/it.json
@@ -5,12 +5,14 @@
},
"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"
}
}
diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json
index 17dee71d640..0235c446e75 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 76f6a7f233c..9bd337962c0 100644
--- a/homeassistant/components/nightscout/translations/lb.json
+++ b/homeassistant/components/nightscout/translations/lb.json
@@ -5,14 +5,17 @@
},
"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)"
}
}
}
diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json
index a586083f569..7f600268e88 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 00000000000..a307f3fc77d
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json
index cf904f0134c..738c4dfa9a3 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 fa9f3d12427..5066f5a2edb 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 d2c08643c06..798972fe384 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 e798b538565..3702688eb94 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 @@ _LOGGER = logging.getLogger(__name__)
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/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py
index 5e9a9835bf4..b6c0d1a5d9b 100644
--- a/homeassistant/components/nsw_fuel_station/sensor.py
+++ b/homeassistant/components/nsw_fuel_station/sensor.py
@@ -7,7 +7,7 @@ from nsw_fuel import FuelCheckClient, FuelCheckError
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 @@ class StationPriceSensor(Entity):
@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/translations/fr.json b/homeassistant/components/nuheat/translations/fr.json
index da5c3260f2a..f0e912805ed 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/pl.json b/homeassistant/components/nuheat/translations/pl.json
index d55b545d040..d992afe9cc0 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/numato/__init__.py b/homeassistant/components/numato/__init__.py
index 8efc56e12fe..0f088cd179d 100644
--- a/homeassistant/components/numato/__init__.py
+++ b/homeassistant/components/numato/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
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 @@ CONF_DST_UNIT = "unit"
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 @@ ADC_SCHEMA = vol.Schema(
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,
}
)
diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json
index 9bf7f981dca..6fd749442c3 100644
--- a/homeassistant/components/nut/translations/no.json
+++ b/homeassistant/components/nut/translations/no.json
@@ -16,6 +16,7 @@
},
"ups": {
"data": {
+ "alias": "",
"resources": "Ressurser"
},
"title": "Velg UPS som skal overv\u00e5kes"
@@ -24,6 +25,7 @@
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"title": "Koble til NUT-serveren"
diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json
index 5fb9082d676..d24638341d2 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/nws/manifest.json b/homeassistant/components/nws/manifest.json
index 23643699f3a..ef0a35b846a 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/translations/et.json b/homeassistant/components/nws/translations/et.json
new file mode 100644
index 00000000000..a9607835b43
--- /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 86db25ebd11..568179cf9fa 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/pl.json b/homeassistant/components/nws/translations/pl.json
index ab1011d9d56..2671f0408c9 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/weather.py b/homeassistant/components/nws/weather.py
index 3c641447e84..69dac297b1b 100644
--- a/homeassistant/components/nws/weather.py
+++ b/homeassistant/components/nws/weather.py
@@ -318,3 +318,8 @@ class NWSWeather(WeatherEntity):
"""
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/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py
index 127ce02b371..2db3531f879 100644
--- a/homeassistant/components/nx584/binary_sensor.py
+++ b/homeassistant/components/nx584/binary_sensor.py
@@ -8,6 +8,7 @@ import requests
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 130fa0d55b8..467cd8c06db 100644
--- a/homeassistant/components/nzbget/__init__.py
+++ b/homeassistant/components/nzbget/__init__.py
@@ -37,7 +37,7 @@ from .coordinator import NZBGetDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["sensor"]
+PLATFORMS = ["sensor", "switch"]
CONFIG_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py
index ddbc73ca10a..b4133e7550d 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/switch.py b/homeassistant/components/nzbget/switch.py
new file mode 100644
index 00000000000..c4ceaab5ded
--- /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/de.json b/homeassistant/components/nzbget/translations/de.json
new file mode 100644
index 00000000000..6aa89d2e62e
--- /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/ko.json b/homeassistant/components/nzbget/translations/ko.json
new file mode 100644
index 00000000000..ea9108b9367
--- /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 00000000000..5da36a5d859
--- /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 00000000000..472952ad8b1
--- /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/pl.json b/homeassistant/components/nzbget/translations/pl.json
index a5bd1b5cdcb..6b39cde94a5 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": {
diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py
new file mode 100644
index 00000000000..ff4dd93a0e1
--- /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 00000000000..791d81b6757
--- /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 00000000000..641ec5a8d94
--- /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 00000000000..a57ef2b062a
--- /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 00000000000..468b48d620a
--- /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 00000000000..f4bb0f45d5e
--- /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 00000000000..285bc29b802
--- /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 00000000000..53c8755dd36
--- /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 00000000000..c4002834589
--- /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 00000000000..bf9cc6abba4
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/el.json
@@ -0,0 +1,12 @@
+{
+ "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 00000000000..858cfe31323
--- /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 00000000000..849cd73b40f
--- /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/fr.json b/homeassistant/components/omnilogic/translations/fr.json
new file mode 100644
index 00000000000..167beb756a8
--- /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 00000000000..38ace995177
--- /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 00000000000..686ca520bff
--- /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 00000000000..6644f439b19
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/lb.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "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 00000000000..2f7e9cfbd12
--- /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 00000000000..ebadb3a40e4
--- /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 00000000000..4b9e07cb01f
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..3b05c74695d
--- /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 00000000000..335e26ced8c
--- /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 81e88e99edb..e2fb8e084b8 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 a2a4fb15fd7..0faf099b9bf 100644
--- a/homeassistant/components/onboarding/views.py
+++ b/homeassistant/components/onboarding/views.py
@@ -159,6 +159,14 @@ class CoreConfigOnboardingView(_BaseOnboardingView):
"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 ac5d1393378..21dc0c2ead3 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 00000000000..af68135af10
--- /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 148c596e130..2ac00c814b2 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
ELECTRICAL_CURRENT_AMPERE,
+ LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
VOLT,
@@ -19,12 +20,15 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
+from .const import (
+ CONF_MOUNT_DIR,
+ CONF_NAMES,
+ DEFAULT_OWSERVER_PORT,
+ DEFAULT_SYSBUS_MOUNT_DIR,
+)
+
_LOGGER = logging.getLogger(__name__)
-CONF_MOUNT_DIR = "mount_dir"
-CONF_NAMES = "names"
-
-DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/"
DEVICE_SENSORS = {
# Family : { SensorType: owfs path }
"10": {"temperature": "temperature"},
@@ -33,6 +37,10 @@ DEVICE_SENSORS = {
"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 @@ SENSOR_TYPES = {
# 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 @@ SENSOR_TYPES = {
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 @@ class OneWire(Entity):
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 @@ class OneWireProxy(OneWire):
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 @@ class OneWireDirect(OneWire):
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 964c7a70a6d..cf92f3df3ba 100644
--- a/homeassistant/components/onvif/__init__.py
+++ b/homeassistant/components/onvif/__init__.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
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 @@ async def _get_snapshot_auth(hass, device, entry):
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/translations/no.json b/homeassistant/components/onvif/translations/no.json
index dcb3a102829..4f605a518d7 100644
--- a/homeassistant/components/onvif/translations/no.json
+++ b/homeassistant/components/onvif/translations/no.json
@@ -34,7 +34,8 @@
"manual_input": {
"data": {
"host": "Vert",
- "name": "Navn"
+ "name": "Navn",
+ "port": ""
},
"title": "Konfigurere ONVIF-enhet"
},
diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json
index d60d45c746f..a2d25de2194 100644
--- a/homeassistant/components/onvif/translations/pl.json
+++ b/homeassistant/components/onvif/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": "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.",
diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json
index 1fb7096d5fa..24b84e305e7 100644
--- a/homeassistant/components/opencv/manifest.json
+++ b/homeassistant/components/opencv/manifest.json
@@ -2,6 +2,6 @@
"domain": "opencv",
"name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/opencv",
- "requirements": ["numpy==1.19.1", "opencv-python-headless==4.3.0.36"],
+ "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"],
"codeowners": []
}
diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py
index 0b696ed9339..3ff1577c436 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 @@ DATA_OPENTHERM_GW = "opentherm_gw"
DEVICE_CLASS_COLD = "cold"
DEVICE_CLASS_HEAT = "heat"
-DEVICE_CLASS_PROBLEM = "problem"
DOMAIN = "opentherm_gw"
@@ -39,7 +40,6 @@ SERVICE_SET_MAX_MOD = "set_max_modulation"
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 @@ SENSOR_INFO = {
"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/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json
index ed4dbd4abfb..f0ecf0277b2 100644
--- a/homeassistant/components/opentherm_gw/translations/no.json
+++ b/homeassistant/components/opentherm_gw/translations/no.json
@@ -10,8 +10,10 @@
"init": {
"data": {
"device": "Bane eller URL-adresse",
+ "id": "",
"name": "Navn"
- }
+ },
+ "title": ""
}
}
},
diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json
index 0342dd3ebcb..960e3a9cf5c 100644
--- a/homeassistant/components/opentherm_gw/translations/pt.json
+++ b/homeassistant/components/opentherm_gw/translations/pt.json
@@ -3,6 +3,7 @@
"step": {
"init": {
"data": {
+ "id": "",
"name": "Nome"
}
}
diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json
new file mode 100644
index 00000000000..aae3ef835bb
--- /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/openweathermap/const.py b/homeassistant/components/openweathermap/const.py
index bc7a428f366..03ed97d4075 100644
--- a/homeassistant/components/openweathermap/const.py
+++ b/homeassistant/components/openweathermap/const.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
PRESSURE_PA,
SPEED_METERS_PER_SECOND,
@@ -72,7 +73,57 @@ FORECAST_MONITORED_CONDITIONS = [
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 @@ WEATHER_SENSOR_TYPES = {
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/translations/ca.json b/homeassistant/components/openweathermap/translations/ca.json
new file mode 100644
index 00000000000..240e378c0fa
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/ca.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La integraci\u00f3 OpenWeatherMap per a aquestes coordenades ja est\u00e0 configurada."
+ },
+ "error": {
+ "auth": "La clau API no \u00e9s correcta.",
+ "connection": "No s'ha pogut connectar amb l'API d'OWM"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API d'OpenWeatherMap",
+ "language": "Idioma",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "mode": "Mode",
+ "name": "Nom de la integraci\u00f3"
+ },
+ "description": "Configura la integraci\u00f3 OpenWeatherMap. Per generar la clau API, ves a https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Idioma",
+ "mode": "Mode"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json
new file mode 100644
index 00000000000..6582b2046b8
--- /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 00000000000..dd34c67ce42
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/es.json b/homeassistant/components/openweathermap/translations/es.json
new file mode 100644
index 00000000000..7dbbd400360
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/es.json
@@ -0,0 +1,35 @@
+{
+ "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.",
+ "connection": "No se puede conectar a la API de OWM"
+ },
+ "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 00000000000..3620f1a363e
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/et.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Nende koordinaatidele on OpenWeatherMapi sidumine juba tehtud."
+ },
+ "error": {
+ "auth": "API v\u00f5ti on vale.",
+ "connection": "OWM API-ga ei saa \u00fchendust luua"
+ },
+ "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 b55997e1f8d..ab53d663f48 100644
--- a/homeassistant/components/openweathermap/translations/fr.json
+++ b/homeassistant/components/openweathermap/translations/fr.json
@@ -1,17 +1,23 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'int\u00e9gration OpenWeatherMap pour ces coordonn\u00e9es est d\u00e9j\u00e0 configur\u00e9e."
+ },
"error": {
- "auth": "La cl\u00e9 API n'est pas correcte."
+ "auth": "La cl\u00e9 API n'est pas correcte.",
+ "connection": "Impossible de se connecter \u00e0 l'API OWM"
},
"step": {
"user": {
"data": {
+ "api_key": "Cl\u00e9 d'API OpenWeatherMap",
"language": "Langue",
"latitude": "Latitude",
"longitude": "Longitude",
"mode": "Mode",
"name": "Nom de l'int\u00e9gration"
},
+ "description": "Configurez l'int\u00e9gration OpenWeatherMap. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://openweathermap.org/appid",
"title": "OpenWeatherMap"
}
}
diff --git a/homeassistant/components/openweathermap/translations/it.json b/homeassistant/components/openweathermap/translations/it.json
new file mode 100644
index 00000000000..c53e88d9558
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/it.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'integrazione di OpenWeatherMap per queste coordinate \u00e8 gi\u00e0 configurata."
+ },
+ "error": {
+ "auth": "La chiave API non \u00e8 corretta.",
+ "connection": "Impossibile connettersi all'API OWM"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API OpenWeatherMap",
+ "language": "Lingua",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "mode": "Modalit\u00e0",
+ "name": "Nome dell'integrazione"
+ },
+ "description": "Configura l'integrazione di OpenWeatherMap. Per generare la chiave API, vai su https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Lingua",
+ "mode": "Modalit\u00e0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json
new file mode 100644
index 00000000000..12e76d85506
--- /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 00000000000..1bf8fb29988
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/lb.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "auth": "Api Schl\u00ebssel ass net korrekt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API Schl\u00ebssel",
+ "language": "Sproch",
+ "latitude": "Breedegrad",
+ "longitude": "L\u00e4ngegrad",
+ "mode": "Modus",
+ "name": "Numm vun der Integratioun"
+ },
+ "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/nb.json b/homeassistant/components/openweathermap/translations/nb.json
new file mode 100644
index 00000000000..f62cc08ac81
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/nb.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "OpenWeatherMap-integrering for disse koordinatene er allerede konfigurert."
+ },
+ "error": {
+ "auth": "API-n\u00f8kkelen er ikke riktig."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mode": "Modus"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Spr\u00e5k",
+ "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 00000000000..fdff089bddb
--- /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
new file mode 100644
index 00000000000..cda3666ff18
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/no.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "OpenWeatherMap-integrasjon for disse koordinatene er allerede konfigurert."
+ },
+ "error": {
+ "auth": "API-n\u00f8kkelen er ikke korrekt.",
+ "connection": "Kan ikke koble til OWM API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "OpenWeatherMap API-n\u00f8kkel",
+ "language": "Spr\u00e5k",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "mode": "Modus",
+ "name": "Navn p\u00e5 integrasjon"
+ },
+ "description": "Sett opp OpenWeatherMap-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Spr\u00e5k",
+ "mode": "Modus"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/pt.json b/homeassistant/components/openweathermap/translations/pt.json
new file mode 100644
index 00000000000..fa7fdc63989
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/pt.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "error": {
+ "auth": "A chave da API n\u00e3o est\u00e1 correta.",
+ "connection": "N\u00e3o \u00e9 poss\u00edvel conectar \u00e0 API OWM"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "language": "Idioma",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "mode": "Modo",
+ "name": "Nome da integra\u00e7\u00e3o"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Idioma",
+ "mode": "Modo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json
new file mode 100644
index 00000000000..a6fe05a8346
--- /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/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json
new file mode 100644
index 00000000000..6f398062876
--- /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 2a1ce2d8d73..b900e75787f 100644
--- a/homeassistant/components/ovo_energy/translations/fr.json
+++ b/homeassistant/components/ovo_energy/translations/fr.json
@@ -1,10 +1,17 @@
{
"config": {
"error": {
+ "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "authorization_error": "Erreur d'autorisation. V\u00e9rifiez vos identifiants.",
"connection_error": "\u00c9chec de connexion"
},
"step": {
"user": {
+ "data": {
+ "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 00000000000..f5481afa94a
--- /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 09d002bc161..7d6882c2bd3 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 00000000000..4d00f0bfc74
--- /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 42afe86d48a..7ab3219f18c 100644
--- a/homeassistant/components/ovo_energy/translations/pl.json
+++ b/homeassistant/components/ovo_energy/translations/pl.json
@@ -1,7 +1,16 @@
{
"config": {
"error": {
- "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py
index 24dc99de71c..d3091d7d027 100644
--- a/homeassistant/components/owntracks/__init__.py
+++ b/homeassistant/components/owntracks/__init__.py
@@ -9,7 +9,12 @@ import voluptuous as vol
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 @@ class OwnTracksContext:
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/messages.py b/homeassistant/components/owntracks/messages.py
index 5e610d861fe..3a4aac6bfd1 100644
--- a/homeassistant/components/owntracks/messages.py
+++ b/homeassistant/components/owntracks/messages.py
@@ -10,7 +10,7 @@ from homeassistant.components.device_tracker import (
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/ozw/climate.py b/homeassistant/components/ozw/climate.py
index 1486d98de2c..a74fd869f0f 100644
--- a/homeassistant/components/ozw/climate.py
+++ b/homeassistant/components/ozw/climate.py
@@ -5,6 +5,7 @@ from typing import Optional, Tuple
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
+ ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
@@ -252,6 +253,11 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
Must know if single or double setpoint.
"""
+ hvac_mode = kwargs.get(ATTR_HVAC_MODE)
+
+ if hvac_mode is not None:
+ await self.async_set_hvac_mode(hvac_mode)
+
if len(self._current_mode_setpoint_values) == 1:
setpoint = self._current_mode_setpoint_values[0]
target_temp = kwargs.get(ATTR_TEMPERATURE)
diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py
index 8bbb6741020..a83f763c810 100644
--- a/homeassistant/components/ozw/discovery.py
+++ b/homeassistant/components/ozw/discovery.py
@@ -136,6 +136,7 @@ DISCOVERY_SCHEMAS = (
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/translations/et.json b/homeassistant/components/ozw/translations/et.json
new file mode 100644
index 00000000000..a167a4edd31
--- /dev/null
+++ b/homeassistant/components/ozw/translations/et.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "mqtt_required": "MQTT sidumine pole seadistatud"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/panasonic_viera/translations/fr.json b/homeassistant/components/panasonic_viera/translations/fr.json
index 4ee07e94ad4..9f8c9b672e5 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/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json
index 91a01793c1c..039adbd2ad3 100644
--- a/homeassistant/components/panasonic_viera/translations/no.json
+++ b/homeassistant/components/panasonic_viera/translations/no.json
@@ -11,6 +11,9 @@
},
"step": {
"pairing": {
+ "data": {
+ "pin": ""
+ },
"description": "Angi PIN-koden som vises p\u00e5 TV-en",
"title": "Sammenkobling"
},
@@ -23,5 +26,6 @@
"title": "Sett opp TV-en din"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py
new file mode 100644
index 00000000000..07ec2cfe985
--- /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/person/translations/nb.json b/homeassistant/components/person/translations/nb.json
index 98c0b9241fb..6d380619114 100644
--- a/homeassistant/components/person/translations/nb.json
+++ b/homeassistant/components/person/translations/nb.json
@@ -4,5 +4,6 @@
"home": "Hjemme",
"not_home": "Borte"
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/person/translations/no.json b/homeassistant/components/person/translations/no.json
index 98c0b9241fb..6d380619114 100644
--- a/homeassistant/components/person/translations/no.json
+++ b/homeassistant/components/person/translations/no.json
@@ -4,5 +4,6 @@
"home": "Hjemme",
"not_home": "Borte"
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json
index 0e56dfaa0d9..1ccc5ac7d76 100644
--- a/homeassistant/components/pi_hole/translations/fr.json
+++ b/homeassistant/components/pi_hole/translations/fr.json
@@ -11,6 +11,7 @@
"data": {
"api_key": "Cl\u00e9 d'API",
"host": "H\u00f4te",
+ "location": "Emplacement",
"name": "Nom",
"port": "Port",
"ssl": "Utiliser SSL",
diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json
index 4655d254070..387b6c0d1eb 100644
--- a/homeassistant/components/pi_hole/translations/no.json
+++ b/homeassistant/components/pi_hole/translations/no.json
@@ -13,6 +13,7 @@
"host": "Vert",
"location": "Beliggenhet",
"name": "Navn",
+ "port": "",
"ssl": "Bruk SSL",
"verify_ssl": "Verifisere SSL-sertifikat"
}
diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json
index b974e6c04c9..394a24a5050 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/pilight/light.py b/homeassistant/components/pilight/light.py
index 12d175817d7..11eeb02293e 100644
--- a/homeassistant/components/pilight/light.py
+++ b/homeassistant/components/pilight/light.py
@@ -61,7 +61,20 @@ class PilightLight(PilightBaseDevice, LightEntity):
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 79e0268c77b..afbfe80b43f 100644
--- a/homeassistant/components/ping/binary_sensor.py
+++ b/homeassistant/components/ping/binary_sensor.py
@@ -10,7 +10,11 @@ from typing import Any, Dict
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 @@ CONF_PING_COUNT = "count"
DEFAULT_NAME = "Ping"
DEFAULT_PING_COUNT = 5
-DEFAULT_DEVICE_CLASS = "connectivity"
SCAN_INTERVAL = timedelta(minutes=5)
@@ -94,7 +97,7 @@ class PingBinarySensor(BinarySensorEntity):
@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:
diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py
index d78b12c06e0..1cb2416d12a 100644
--- a/homeassistant/components/plant/__init__.py
+++ b/homeassistant/components/plant/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
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/plant/translations/nb.json b/homeassistant/components/plant/translations/nb.json
index 0d144184263..c8f9e3e1d44 100644
--- a/homeassistant/components/plant/translations/nb.json
+++ b/homeassistant/components/plant/translations/nb.json
@@ -1,6 +1,7 @@
{
"state": {
"_": {
+ "ok": "",
"problem": "Problem"
}
},
diff --git a/homeassistant/components/plant/translations/no.json b/homeassistant/components/plant/translations/no.json
index 0a08a5eaed4..e82299e36e9 100644
--- a/homeassistant/components/plant/translations/no.json
+++ b/homeassistant/components/plant/translations/no.json
@@ -1,3 +1,9 @@
{
+ "state": {
+ "_": {
+ "ok": "",
+ "problem": ""
+ }
+ },
"title": "Plantemonitor"
}
\ No newline at end of file
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index 4e5abad4f79..648e7b7706a 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -5,7 +5,14 @@ import json
import logging
import plexapi.exceptions
-from plexwebsocket import PlexWebsocket
+from plexwebsocket import (
+ SIGNAL_CONNECTION_STATE,
+ SIGNAL_DATA,
+ STATE_CONNECTED,
+ STATE_DISCONNECTED,
+ STATE_STOPPED,
+ PlexWebsocket,
+)
import requests.exceptions
import voluptuous as vol
@@ -14,12 +21,15 @@ from homeassistant.components.media_player.const import (
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:
- _LOGGER.error(
- "Plex server (%s) could not be reached: [%s]",
- server_config[CONF_URL],
- error,
- )
+ if entry.state != ENTRY_STATE_SETUP_RETRY:
+ _LOGGER.error(
+ "Plex server (%s) could not be reached: [%s]",
+ server_config[CONF_URL],
+ error,
+ )
raise ConfigEntryNotReady from error
+ except plexapi.exceptions.Unauthorized:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ PLEX_DOMAIN,
+ context={CONF_SOURCE: SOURCE_REAUTH},
+ data={**entry.data, "config_entry_id": entry.entry_id},
+ )
+ )
+ _LOGGER.error(
+ "Token not accepted, please reauthenticate Plex server '%s'",
+ entry.data[CONF_SERVER],
+ )
+ 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 ffadba63d3a..b2bf856402e 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
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,
)
@@ -95,6 +96,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.token = None
self.client_id = None
self._manual = False
+ self._entry_id = None
async def async_step_user(
self, user_input=None, errors=None
@@ -209,10 +211,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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 +224,28 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL
)
+ data = {
+ CONF_SERVER: plex_server.friendly_name,
+ CONF_SERVER_IDENTIFIER: server_id,
+ PLEX_SERVER_CONFIG: entry_config,
+ }
+
+ await self.async_set_unique_id(server_id)
+ if (
+ self.context[CONF_SOURCE] # pylint: disable=no-member
+ == config_entries.SOURCE_REAUTH
+ ):
+ entry = self.hass.config_entries.async_get_entry(self._entry_id)
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ 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 +326,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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)
+ self._entry_id = self.current_login.pop("config_entry_id")
+ 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 bbf7be9914e..f5bbc6ac53c 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/server.py b/homeassistant/components/plex/server.py
index f8706eadf22..a5ac287328e 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -62,9 +62,12 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION
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 @@ class PlexServer:
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 2f50e2d3090..1f9226ff776 100644
--- a/homeassistant/components/plex/strings.json
+++ b/homeassistant/components/plex/strings.json
@@ -41,8 +41,9 @@
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured",
+ "reauth_successful": "Successfully reauthenticated",
"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 be4b6215f8b..01972d383b5 100644
--- a/homeassistant/components/plex/translations/ca.json
+++ b/homeassistant/components/plex/translations/ca.json
@@ -4,8 +4,9 @@
"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",
+ "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",
diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json
index b14e3a3c574..961ad4b3ed6 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 00000000000..54f64b814fd
--- /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 83e5196fc35..826f71c1244 100644
--- a/homeassistant/components/plex/translations/en.json
+++ b/homeassistant/components/plex/translations/en.json
@@ -4,8 +4,9 @@
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured",
+ "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",
diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json
index 907025590c6..cc5f4569020 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 b0996fad3d7..dbc4a272e5d 100644
--- a/homeassistant/components/plex/translations/it.json
+++ b/homeassistant/components/plex/translations/it.json
@@ -5,7 +5,7 @@
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
"already_in_progress": "Plex \u00e8 in fase di configurazione",
"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",
diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json
index 3a01e3f67c1..eda0cb04b03 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 027260ea34d..b279eef01f7 100644
--- a/homeassistant/components/plex/translations/no.json
+++ b/homeassistant/components/plex/translations/no.json
@@ -4,8 +4,9 @@
"all_configured": "Alle knyttet servere som allerede er konfigurert",
"already_configured": "Denne Plex-serveren er allerede konfigurert",
"already_in_progress": "Plex blir konfigurert",
+ "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",
@@ -19,6 +20,7 @@
"manual_setup": {
"data": {
"host": "Vert",
+ "port": "",
"ssl": "Bruk SSL",
"token": "Token (valgfritt)",
"verify_ssl": "Verifisere SSL-sertifikat"
@@ -26,16 +28,21 @@
"title": "Manuell Plex-konfigurasjon"
},
"select_server": {
+ "data": {
+ "server": ""
+ },
"description": "Flere servere tilgjengelig, velg en:",
"title": "Velg Plex-server"
},
"user": {
- "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server."
+ "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.",
+ "title": ""
},
"user_advanced": {
"data": {
"setup_method": "Oppsettmetode"
- }
+ },
+ "title": ""
}
}
},
diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json
index 29937ccea5f..6229eac7d24 100644
--- a/homeassistant/components/plex/translations/ru.json
+++ b/homeassistant/components/plex/translations/ru.json
@@ -4,8 +4,9 @@
"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.",
+ "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.",
diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json
index 2d866880dec..3f41f286b30 100644
--- a/homeassistant/components/plex/translations/zh-Hant.json
+++ b/homeassistant/components/plex/translations/zh-Hant.json
@@ -4,8 +4,9 @@
"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",
+ "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",
diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py
index 0e55c3e715c..f7986f91540 100644
--- a/homeassistant/components/plugwise/__init__.py
+++ b/homeassistant/components/plugwise/__init__.py
@@ -10,7 +10,7 @@ import async_timeout
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 @@ from homeassistant.helpers.update_coordinator import (
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,
)
diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index 689bfb68f22..14405062231 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -5,12 +5,16 @@ from Plugwise_Smile.Smile import Smile
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 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# 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,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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]:
diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json
index 7dc8542698b..0abcd780255 100644
--- a/homeassistant/components/plugwise/strings.json
+++ b/homeassistant/components/plugwise/strings.json
@@ -13,10 +13,11 @@
"step": {
"user": {
"title": "Connect to the Smile",
- "description": "Details",
+ "description": "Please enter:",
"data": {
+ "password": "Smile ID",
"host": "Smile IP address",
- "password": "Smile ID"
+ "port": "Smile port number"
}
}
},
diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json
index 40a7a0da317..0477c6e1dad 100644
--- a/homeassistant/components/plugwise/translations/ca.json
+++ b/homeassistant/components/plugwise/translations/ca.json
@@ -13,11 +13,22 @@
"user": {
"data": {
"host": "Adre\u00e7a IP de Smile",
- "password": "ID de Smile"
+ "password": "ID de Smile",
+ "port": "N\u00famero de port de Smile"
},
- "description": "Detalls",
+ "description": "Introdueix:",
"title": "Connecta't amb el Smile"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval d'escaneig (segons)"
+ },
+ "description": "Ajusta les opcions de Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json
new file mode 100644
index 00000000000..ca6cd85a20f
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/cs.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval sledov\u00e1n\u00ed (v sekund\u00e1ch)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json
index 7ee92b1ead5..19d3678b5e8 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 00000000000..cdd3c177cb7
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json
index 238f435f3ab..8da8694f872 100644
--- a/homeassistant/components/plugwise/translations/en.json
+++ b/homeassistant/components/plugwise/translations/en.json
@@ -13,9 +13,10 @@
"user": {
"data": {
"host": "Smile IP address",
- "password": "Smile ID"
+ "password": "Smile ID",
+ "port": "Smile port number"
},
- "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 31e876cfe3a..9ab00348f81 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 00000000000..a1a5d42db59
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/et.json
@@ -0,0 +1,12 @@
+{
+ "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 392d990fbbf..fe4b88ab0f1 100644
--- a/homeassistant/components/plugwise/translations/fr.json
+++ b/homeassistant/components/plugwise/translations/fr.json
@@ -13,11 +13,22 @@
"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"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervalle de mise \u00e0 jour (secondes)"
+ },
+ "description": "Ajuster les options Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json
index b6c03cf8899..0a215c39f65 100644
--- a/homeassistant/components/plugwise/translations/it.json
+++ b/homeassistant/components/plugwise/translations/it.json
@@ -13,11 +13,22 @@
"user": {
"data": {
"host": "Indirizzo IP Smile",
- "password": "ID Smile"
+ "password": "ID Smile",
+ "port": "Numero porta Smile"
},
- "description": "Dettagli",
+ "description": "Si prega di inserire:",
"title": "Connettersi al dispositivo"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Intervallo di scansione (secondi)"
+ },
+ "description": "Regolare le opzioni Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json
index df480242f6f..04fcb738285 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 8b0ea38c2f6..cd2100804a0 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 964675e0c63..5d0bd789957 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 694e6348cae..4902ada06c2 100644
--- a/homeassistant/components/plugwise/translations/no.json
+++ b/homeassistant/components/plugwise/translations/no.json
@@ -8,14 +8,27 @@
"invalid_auth": "Ugyldig godkjenning, sjekk din 8-tegns Smile ID",
"unknown": "Uventet feil"
},
+ "flow_title": "",
"step": {
"user": {
"data": {
- "host": "Smile IP-adresse"
+ "host": "Smile IP-adresse",
+ "password": "",
+ "port": "Smil portnummer"
},
- "description": "Detaljer",
+ "description": "Vennligst skriv inn:",
"title": "Koble til Smile"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Skanneintervall (sekunder)"
+ },
+ "description": "Juster Plugwise-alternativer"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json
index 135b9d838fc..16996161f09 100644
--- a/homeassistant/components/plugwise/translations/pl.json
+++ b/homeassistant/components/plugwise/translations/pl.json
@@ -6,7 +6,7 @@
"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": {
diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json
index 0c5c7760566..808e3f3f7ea 100644
--- a/homeassistant/components/plugwise/translations/pt.json
+++ b/homeassistant/components/plugwise/translations/pt.json
@@ -3,5 +3,14 @@
"error": {
"unknown": "Erro inesperado"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (segundos)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json
index 650ae69a972..5d3afb061fc 100644
--- a/homeassistant/components/plugwise/translations/ru.json
+++ b/homeassistant/components/plugwise/translations/ru.json
@@ -13,11 +13,22 @@
"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"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Plugwise"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json
index 8cc068681c3..58dd71266b3 100644
--- a/homeassistant/components/plugwise/translations/zh-Hant.json
+++ b/homeassistant/components/plugwise/translations/zh-Hant.json
@@ -13,11 +13,22 @@
"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"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09"
+ },
+ "description": "\u8abf\u6574 Plugwise \u9078\u9805"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json
new file mode 100644
index 00000000000..f55df964f86
--- /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 00000000000..436e8b1fb7d
--- /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 121744d0f0d..83d814d65dc 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 5a780c2e57a..d82ecd096ee 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 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity):
@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 4ac8f0c1832..9436877e434 100644
--- a/homeassistant/components/point/sensor.py
+++ b/homeassistant/components/point/sensor.py
@@ -7,6 +7,7 @@ from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
+ PRESSURE_HPA,
TEMP_CELSIUS,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -21,7 +22,7 @@ DEVICE_CLASS_SOUND = "sound_level"
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/es.json b/homeassistant/components/point/translations/es.json
index a7247b3d9b3..5374d2808d9 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 1596ba05916..286c9e67fc8 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 d54c09e3cef..8fc660e8128 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 00000000000..3b2d79a34a7
--- /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/no.json b/homeassistant/components/poolsense/translations/no.json
index a199f7384a8..38adc04c1db 100644
--- a/homeassistant/components/poolsense/translations/no.json
+++ b/homeassistant/components/poolsense/translations/no.json
@@ -12,7 +12,8 @@
"email": "E-post",
"password": "Passord"
},
- "description": "[%key:common::config_flow::description%]"
+ "description": "[%key:common::config_flow::description%]",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/poolsense/translations/pl.json b/homeassistant/components/poolsense/translations/pl.json
index d463be1c5dd..54f1a80b043 100644
--- a/homeassistant/components/poolsense/translations/pl.json
+++ b/homeassistant/components/poolsense/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": {
- "invalid_auth": "Niepoprawne uwierzytelnienie."
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json
index 20eb71e7c27..cc56f1e116d 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/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json
new file mode 100644
index 00000000000..f772a8586d0
--- /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/hu.json b/homeassistant/components/progettihwsw/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..b8b78de069c
--- /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 00000000000..e5ec74b7ab0
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/lb.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "step": {
+ "relay_modes": {
+ "data": {
+ "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": "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 00000000000..2b30a4f1caa
--- /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 ee25c598ecd..cdfea7a9221 100644
--- a/homeassistant/components/progettihwsw/translations/pl.json
+++ b/homeassistant/components/progettihwsw/translations/pl.json
@@ -1,9 +1,33 @@
{
"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"
},
"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"
+ }
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index 87a8a2c41f3..bd9a6e35276 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -19,7 +19,9 @@ from homeassistant.components.humidifier.const import (
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 @@ class PrometheusMetrics:
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 @@ class PrometheusMetrics:
"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 7beaaaf00e1..2d0d14a69c1 100644
--- a/homeassistant/components/proximity/__init__.py
+++ b/homeassistant/components/proximity/__init__.py
@@ -4,6 +4,8 @@ import logging
import voluptuous as vol
from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
CONF_DEVICES,
CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE,
@@ -149,8 +151,8 @@ class Proximity(Entity):
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 @@ class Proximity(Entity):
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 @@ class Proximity(Entity):
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/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json
index 4bf3b02b0b5..814f09095a2 100644
--- a/homeassistant/components/ps4/translations/no.json
+++ b/homeassistant/components/ps4/translations/no.json
@@ -15,21 +15,26 @@
},
"step": {
"creds": {
- "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette."
+ "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette.",
+ "title": ""
},
"link": {
"data": {
+ "code": "",
"ip_address": "IP adresse",
- "name": "Navn"
+ "name": "Navn",
+ "region": ""
},
- "description": "Fyll inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsoll. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Fyll inn PIN-koden som vises. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer informasjon."
+ "description": "Fyll inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsoll. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Fyll inn PIN-koden som vises. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer informasjon.",
+ "title": ""
},
"mode": {
"data": {
"ip_address": "IP-adresse (La st\u00e5 tom hvis du bruker Automatisk Oppdagelse).",
"mode": "Konfigureringsmodus"
},
- "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk."
+ "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py
index 25b4531cee0..a9b53c970bd 100644
--- a/homeassistant/components/pvpc_hourly_pricing/sensor.py
+++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py
@@ -6,7 +6,7 @@ from typing import Optional
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 @@ _LOGGER = logging.getLogger(__name__)
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/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json
index 4048e357751..5386529e43a 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/rachio/translations/pl.json b/homeassistant/components/rachio/translations/pl.json
index e077fea03a4..55e8d18fe1c 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/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py
index 6f87c34d607..8534748978d 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 @@ SENSORS = {
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/translations/no.json b/homeassistant/components/rainmachine/translations/no.json
index 294c2726396..bc80cdedb31 100644
--- a/homeassistant/components/rainmachine/translations/no.json
+++ b/homeassistant/components/rainmachine/translations/no.json
@@ -11,7 +11,8 @@
"user": {
"data": {
"ip_address": "Vertsnavn eller IP-adresse",
- "password": "Passord"
+ "password": "Passord",
+ "port": ""
},
"title": "Fyll ut informasjonen din"
}
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 9ce950fedfe..63d466fa4d1 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -196,6 +196,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
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 @@ class Recorder(threading.Thread):
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 @@ class Recorder(threading.Thread):
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 @@ class Recorder(threading.Thread):
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:
@@ -377,7 +386,6 @@ class Recorder(threading.Thread):
if event.event_type == EVENT_STATE_CHANGED:
dbevent.event_data = "{}"
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 +396,15 @@ class Recorder(threading.Thread):
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 +490,12 @@ class Recorder(threading.Thread):
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 +519,9 @@ class Recorder(threading.Thread):
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 b5384cf84cb..642407cf0c6 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -14,6 +14,7 @@ from sqlalchemy import (
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
@@ -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 82ba4812592..6f91e2a9abe 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 00000000000..1636054663d
--- /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/ca.json b/homeassistant/components/remote/translations/ca.json
index 94ff71f6d92..7e001059f14 100644
--- a/homeassistant/components/remote/translations/ca.json
+++ b/homeassistant/components/remote/translations/ca.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Commuta {entity_name}",
+ "turn_off": "Apaga {entity_name}",
+ "turn_on": "Engega {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e0 apagat/ada",
+ "is_on": "{entity_name} est\u00e0 engegat/ada"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} s'ha apagat",
+ "turned_on": "{entity_name} s'ha engegat"
+ }
+ },
"state": {
"_": {
"off": "OFF",
diff --git a/homeassistant/components/remote/translations/cs.json b/homeassistant/components/remote/translations/cs.json
index 098b1191b8f..91102dd4461 100644
--- a/homeassistant/components/remote/translations/cs.json
+++ b/homeassistant/components/remote/translations/cs.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "P\u0159epnout {entity_name}",
+ "turn_off": "Vypnout {entity_name}",
+ "turn_on": "Zapnout {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} je vypnuto",
+ "is_on": "{entity_name} je zapnuto"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} vypnuto",
+ "turned_on": "{entity_name} zapnuto"
+ }
+ },
"state": {
"_": {
"off": "Neaktivn\u00ed",
diff --git a/homeassistant/components/remote/translations/de.json b/homeassistant/components/remote/translations/de.json
index d1ec188e2b8..ffd542f27d9 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/en.json b/homeassistant/components/remote/translations/en.json
index 731a21d4547..dc11ed5917d 100644
--- a/homeassistant/components/remote/translations/en.json
+++ b/homeassistant/components/remote/translations/en.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Toggle {entity_name}",
+ "turn_off": "Turn off {entity_name}",
+ "turn_on": "Turn on {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} is off",
+ "is_on": "{entity_name} is on"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on"
+ }
+ },
"state": {
"_": {
"off": "Off",
diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json
index bf8b6d3a3ec..e2452b80e89 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 6bcfbf7f4cf..458704b9999 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/fr.json b/homeassistant/components/remote/translations/fr.json
index ad2461e1f0a..37d97469645 100644
--- a/homeassistant/components/remote/translations/fr.json
+++ b/homeassistant/components/remote/translations/fr.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Basculer {entity_name}",
+ "turn_off": "\u00c9teindre {entity_name}",
+ "turn_on": "Allumer {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est d\u00e9sactiv\u00e9",
+ "is_on": "{entity_name} est activ\u00e9"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} s'est \u00e9teint",
+ "turned_on": "{entity_name} s'est allum\u00e9"
+ }
+ },
"state": {
"_": {
"off": "Inactif",
diff --git a/homeassistant/components/remote/translations/it.json b/homeassistant/components/remote/translations/it.json
index e3ebc2a3dda..e770712be19 100644
--- a/homeassistant/components/remote/translations/it.json
+++ b/homeassistant/components/remote/translations/it.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Commuta {entity_name}",
+ "turn_off": "Disattivare {entity_name}",
+ "turn_on": "Attivare {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e8 spento",
+ "is_on": "{entity_name} \u00e8 acceso"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} disattivato",
+ "turned_on": "{entity_name} attivato"
+ }
+ },
"state": {
"_": {
"off": "Spento",
diff --git a/homeassistant/components/remote/translations/ko.json b/homeassistant/components/remote/translations/ko.json
index b866fd7fee5..bd055e21f5b 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 b81e82470fc..f9f81a85a29 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/nb.json b/homeassistant/components/remote/translations/nb.json
index 2e65d515e59..f8bff2524cd 100644
--- a/homeassistant/components/remote/translations/nb.json
+++ b/homeassistant/components/remote/translations/nb.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Aktiv\u00e9r/deaktiv\u00e9r {entity_name}",
+ "turn_off": "Sl\u00e5 av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} er av",
+ "is_on": "{entity_name} er p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5"
+ }
+ },
"state": {
"_": {
"off": "Av",
diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json
index b3ccad9ae2b..18d984f5c68 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/no.json b/homeassistant/components/remote/translations/no.json
index 2e65d515e59..5120acfcb61 100644
--- a/homeassistant/components/remote/translations/no.json
+++ b/homeassistant/components/remote/translations/no.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Veksle {entity_name}",
+ "turn_off": "Sl\u00e5 av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} er av",
+ "is_on": "{entity_name} er p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5"
+ }
+ },
"state": {
"_": {
"off": "Av",
diff --git a/homeassistant/components/remote/translations/pt.json b/homeassistant/components/remote/translations/pt.json
index fb303b36aa6..929fd7863bd 100644
--- a/homeassistant/components/remote/translations/pt.json
+++ b/homeassistant/components/remote/translations/pt.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Alternar {entity_name}",
+ "turn_off": "Desligar {entity_name}",
+ "turn_on": "Ligar {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e1 desligada",
+ "is_on": "{entity_name} est\u00e1 ligada"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} desligou-se",
+ "turned_on": "{entity_name} ligou-se"
+ }
+ },
"state": {
"_": {
"off": "Desativado",
diff --git a/homeassistant/components/remote/translations/ru.json b/homeassistant/components/remote/translations/ru.json
index 14dd4a6ec2d..0a725bcca91 100644
--- a/homeassistant/components/remote/translations/ru.json
+++ b/homeassistant/components/remote/translations/ru.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
+ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
+ }
+ },
"state": {
"_": {
"off": "\u0412\u044b\u043a\u043b",
diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json
index ea82df41e75..1b6584c5bf8 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 bc52ed67ae5..2feda4928e5 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/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json
index b387a57723d..2c6c3240dbd 100644
--- a/homeassistant/components/remote/translations/zh-Hant.json
+++ b/homeassistant/components/remote/translations/zh-Hant.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u5207\u63db{entity_name}",
+ "turn_off": "\u95dc\u9589{entity_name}",
+ "turn_on": "\u958b\u555f{entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name}\u70ba\u95dc\u9589",
+ "is_on": "{entity_name}\u70ba\u958b\u555f"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name}\u5df2\u95dc\u9589",
+ "turned_on": "{entity_name}\u5df2\u958b\u555f"
+ }
+ },
"state": {
"_": {
"off": "\u95dc\u9589",
diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py
index 6082d54df12..eb13800f748 100644
--- a/homeassistant/components/rfxtrx/__init__.py
+++ b/homeassistant/components/rfxtrx/__init__.py
@@ -18,11 +18,19 @@ from homeassistant.const import (
CONF_DEVICES,
CONF_HOST,
CONF_PORT,
+ DEGREE,
+ ELECTRICAL_CURRENT_AMPERE,
+ ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_STOP,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
+ SPEED_METERS_PER_SECOND,
TEMP_CELSIUS,
+ TIME_HOURS,
UV_INDEX,
+ VOLT,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@@ -52,30 +60,28 @@ DATA_TYPES = OrderedDict(
("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"),
]
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 4acde6b0450..81e0c60e055 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -9,7 +9,14 @@ from homeassistant.components.sensor import (
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 @@ class RfxtrxSensor(RfxtrxEntity):
"""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/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json
new file mode 100644
index 00000000000..da1d200c2a2
--- /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/fr.json b/homeassistant/components/rfxtrx/translations/fr.json
new file mode 100644
index 00000000000..c4bc0d48b1a
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/fr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ }
+ }
+}
\ 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 00000000000..3b2d79a34a7
--- /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/ko.json b/homeassistant/components/rfxtrx/translations/ko.json
new file mode 100644
index 00000000000..aa8512da285
--- /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/pl.json b/homeassistant/components/rfxtrx/translations/pl.json
new file mode 100644
index 00000000000..637a81a3f87
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/pl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py
index fa303b94378..ccec9e6ad36 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 @@ _LOGGER = logging.getLogger(__name__)
# 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/translations/pl.json b/homeassistant/components/ring/translations/pl.json
index 96aa7d39159..b095647d06c 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/risco/translations/de.json b/homeassistant/components/risco/translations/de.json
new file mode 100644
index 00000000000..b0bae40bf4e
--- /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/hu.json b/homeassistant/components/risco/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..37d9a61307b
--- /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 933af1f4a38..985112bf446 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 00000000000..614a896c3f8
--- /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 9859923d31e..66bc71bc397 100644
--- a/homeassistant/components/risco/translations/pl.json
+++ b/homeassistant/components/risco/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": {
@@ -16,5 +16,17 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "risco_to_ha": {
+ "data": {
+ "A": "Grupa A",
+ "B": "Grupa B",
+ "C": "Grupa C",
+ "D": "Grupa D"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json
index eb4bf7ba6a7..7b98c6234c5 100644
--- a/homeassistant/components/risco/translations/pt.json
+++ b/homeassistant/components/risco/translations/pt.json
@@ -12,6 +12,7 @@
"user": {
"data": {
"password": "Palavra-passe",
+ "pin": "C\u00f3digo PIN",
"username": "Nome de Utilizador"
}
}
diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py
index f6f8c8976f1..b5be3e99d9a 100644
--- a/homeassistant/components/roku/browse_media.py
+++ b/homeassistant/components/roku/browse_media.py
@@ -13,8 +13,13 @@ from homeassistant.components.media_player.const import (
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/no.json b/homeassistant/components/roku/translations/no.json
index 10fb51557da..43e0ea1f1c8 100644
--- a/homeassistant/components/roku/translations/no.json
+++ b/homeassistant/components/roku/translations/no.json
@@ -10,7 +10,8 @@
"flow_title": "Roku: {name}",
"step": {
"ssdp_confirm": {
- "description": "Vil du sette opp {name} ?"
+ "description": "Vil du sette opp {name} ?",
+ "title": ""
},
"user": {
"data": {
diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json
index 7ca0148ce35..21c969876e7 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/translations/fr.json b/homeassistant/components/roomba/translations/fr.json
index a8ff72b2ed5..1ec97dd3842 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/roon/translations/de.json b/homeassistant/components/roon/translations/de.json
new file mode 100644
index 00000000000..b73dc4d6444
--- /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/hu.json b/homeassistant/components/roon/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 50c22e9e256..cdffd6e88ae 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 00000000000..d6cc1c7dd8b
--- /dev/null
+++ b/homeassistant/components/roon/translations/pl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ }
+ }
+}
\ 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 00000000000..993d0b313c0
--- /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 00000000000..79ef36e891a
--- /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 00000000000..9924ebf0440
--- /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 00000000000..98cfc438903
--- /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 00000000000..e0d2a6424e8
--- /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 00000000000..a9cd6c2d907
--- /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 00000000000..c53fa570b7e
--- /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 00000000000..b60cb60f985
--- /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 00000000000..08a3ca23e6b
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/el.json
@@ -0,0 +1,8 @@
+{
+ "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"
+ }
+ },
+ "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 00000000000..6995190979a
--- /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 00000000000..215b15014ab
--- /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 00000000000..350e09ca86f
--- /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 00000000000..7e4fd715ee0
--- /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 00000000000..4e7a14d05e8
--- /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 00000000000..02271833220
--- /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 00000000000..0e08431d1b6
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/lb.json
@@ -0,0 +1,7 @@
+{
+ "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."
+ }
+ }
+}
\ 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 00000000000..a18ff63733e
--- /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 00000000000..63f46667e79
--- /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 00000000000..4f466e3455f
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/pl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
+ }
+ }
+ }
+}
\ 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 00000000000..f91df15e1b3
--- /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 00000000000..37dbb151d8e
--- /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/manifest.json b/homeassistant/components/samsungtv/manifest.json
index efcb9064208..5584d2dd452 100644
--- a/homeassistant/components/samsungtv/manifest.json
+++ b/homeassistant/components/samsungtv/manifest.json
@@ -4,13 +4,15 @@
"documentation": "https://www.home-assistant.io/integrations/samsungtv",
"requirements": [
"samsungctl[websocket]==0.7.1",
- "samsungtvws[websocket]==1.4.0"
+ "samsungtvws==1.4.0"
],
"ssdp": [
{
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
}
],
- "codeowners": ["@escoand"],
+ "codeowners": [
+ "@escoand"
+ ],
"config_flow": true
}
diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json
index 022dddcd00b..e0420ba74af 100644
--- a/homeassistant/components/samsungtv/translations/no.json
+++ b/homeassistant/components/samsungtv/translations/no.json
@@ -10,7 +10,8 @@
"flow_title": "Samsung TV: {model}",
"step": {
"confirm": {
- "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet."
+ "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.",
+ "title": ""
},
"user": {
"data": {
diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py
index 19763903f27..ea9c19376f6 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 @@ class SatelIntegraBinarySensor(BinarySensorEntity):
@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/scene/translations/no.json b/homeassistant/components/scene/translations/no.json
new file mode 100644
index 00000000000..d8a4c453015
--- /dev/null
+++ b/homeassistant/components/scene/translations/no.json
@@ -0,0 +1,3 @@
+{
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/script/translations/no.json b/homeassistant/components/script/translations/no.json
index 6cace1e1570..28122450085 100644
--- a/homeassistant/components/script/translations/no.json
+++ b/homeassistant/components/script/translations/no.json
@@ -4,5 +4,6 @@
"off": "Av",
"on": "P\u00e5"
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/season/translations/sensor.et.json b/homeassistant/components/season/translations/sensor.et.json
new file mode 100644
index 00000000000..eb9953a73ce
--- /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 6dbf4d5c2b7..8d8907f2dcf 100644
--- a/homeassistant/components/sendgrid/notify.py
+++ b/homeassistant/components/sendgrid/notify.py
@@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_RECIPIENT,
CONF_SENDER,
CONTENT_TYPE_TEXT_PLAIN,
+ HTTP_ACCEPTED,
)
import homeassistant.helpers.config_validation as cv
@@ -65,5 +66,5 @@ class SendgridNotificationService(BaseNotificationService):
}
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/translations/pl.json b/homeassistant/components/sense/translations/pl.json
index c32b61e30ad..7cf2ebe4709 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/sensor/group.py b/homeassistant/components/sensor/group.py
new file mode 100644
index 00000000000..4741f8a3b54
--- /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/es-419.json b/homeassistant/components/sensor/translations/es-419.json
index acf91a79104..e724fe3a106 100644
--- a/homeassistant/components/sensor/translations/es-419.json
+++ b/homeassistant/components/sensor/translations/es-419.json
@@ -10,5 +10,11 @@
"value": "{entity_name} cambios de valor"
}
},
+ "state": {
+ "_": {
+ "off": "",
+ "on": ""
+ }
+ },
"title": "Sensor"
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json
index 8238a8b6ab0..450f5b60537 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/nb.json b/homeassistant/components/sensor/translations/nb.json
index 6cace1e1570..28122450085 100644
--- a/homeassistant/components/sensor/translations/nb.json
+++ b/homeassistant/components/sensor/translations/nb.json
@@ -4,5 +4,6 @@
"off": "Av",
"on": "P\u00e5"
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json
index d8d05b81042..e957aef5b87 100644
--- a/homeassistant/components/sensor/translations/no.json
+++ b/homeassistant/components/sensor/translations/no.json
@@ -36,5 +36,6 @@
"off": "Av",
"on": "P\u00e5"
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json
index 56e587bb44c..391415409f5 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 471a349a4df..2bcb38ed168 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.3"],
+ "requirements": ["sentry-sdk==0.18.0"],
"codeowners": ["@dcramer", "@frenck"]
}
diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json
index 5d6e27bd737..6e6d640cd45 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/ko.json b/homeassistant/components/sentry/translations/ko.json
index 7e60891e166..f3adbefa3d7 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/no.json b/homeassistant/components/sentry/translations/no.json
index 48504931104..6048bde74e1 100644
--- a/homeassistant/components/sentry/translations/no.json
+++ b/homeassistant/components/sentry/translations/no.json
@@ -13,7 +13,8 @@
"data": {
"dsn": "DSN"
},
- "description": "Fyll inn din Sentry DNS"
+ "description": "Fyll inn din Sentry DNS",
+ "title": ""
}
}
},
diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json
index fa87b8510c7..00206d3e88b 100644
--- a/homeassistant/components/sentry/translations/pl.json
+++ b/homeassistant/components/sentry/translations/pl.json
@@ -6,7 +6,7 @@
},
"error": {
"bad_dsn": "Nieprawid\u0142owy DSN",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json
new file mode 100644
index 00000000000..5a1d4f2f185
--- /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/et.json b/homeassistant/components/sharkiq/translations/et.json
new file mode 100644
index 00000000000..ff5b447a315
--- /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 00000000000..9f2fd5d72f4
--- /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 00000000000..d7e196c09fb
--- /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 00000000000..b9984968cbf
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/lb.json
@@ -0,0 +1,25 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/nb.json b/homeassistant/components/sharkiq/translations/nb.json
new file mode 100644
index 00000000000..c7b6400d476
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/nb.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "unknown": "Uforventet feil"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..03605190c3c
--- /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 dcb12e86906..283e2c4f440 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 00000000000..75f4175c9af
--- /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 c3b701449c2..c63858f0652 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,
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index c9a13249aa8..1460c62f153 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -20,31 +20,31 @@ SENSORS = {
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 23ba5eb1228..b13c4090a10 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -8,7 +8,12 @@ import async_timeout
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
@@ -93,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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"
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index 96281e2aee1..237deec4da1 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
):
@@ -75,7 +116,7 @@ class ShellyBlockEntity(entity.Entity):
"""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 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
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 e996e43a14a..3816d31222e 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.3"],
+ "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 8a24a6380ed..14c1b645118 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -5,6 +5,7 @@ from homeassistant.const import (
DEGREE,
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
)
@@ -58,6 +59,12 @@ SENSORS = {
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,
@@ -83,6 +90,12 @@ SENSORS = {
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 +117,7 @@ SENSORS = {
),
("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 1a7c8c78189..69a09204227 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": {
diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json
index 7e8c3873c11..41ffcb34b89 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 00000000000..aad0d1fa47d
--- /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 00000000000..c532c3054b7
--- /dev/null
+++ b/homeassistant/components/shelly/translations/el.json
@@ -0,0 +1,12 @@
+{
+ "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."
+ },
+ "step": {
+ "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."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json
index 546007af0d1..2727cfbceeb 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 bdc05b734ba..814586abdae 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 00000000000..3b2d79a34a7
--- /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 595a57b0a00..b1584c675b3 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.",
diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json
new file mode 100644
index 00000000000..5fb84e0ac90
--- /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 b50f528c3c0..714b42e9fcc 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/nb.json b/homeassistant/components/shelly/translations/nb.json
new file mode 100644
index 00000000000..ef07be6f70d
--- /dev/null
+++ b/homeassistant/components/shelly/translations/nb.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json
new file mode 100644
index 00000000000..92a172ff081
--- /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 898c05e89aa..ac5067d3273 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 77a7f045671..1e54f5a0370 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 570e6f8d7c7..24478afe0a4 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 e8fe857c476..ebd7df5f919 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 79189e6b047..21977c286d0 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/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py
index 6df6e1d0c82..1de3cfeb8a0 100644
--- a/homeassistant/components/sigfox/sensor.py
+++ b/homeassistant/components/sigfox/sensor.py
@@ -8,7 +8,7 @@ import requests
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 @@ class SigfoxAPI:
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/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index dd9ab53cb98..613187ef744 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,6 +3,6 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==9.3.0"],
+ "requirements": ["simplisafe-python==9.4.1"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json
index bdfd5d76198..590f4bbc630 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 42fc575f650..6b71d78673c 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 730b9b810d1..54f89eb3ab4 100644
--- a/homeassistant/components/simplisafe/translations/fr.json
+++ b/homeassistant/components/simplisafe/translations/fr.json
@@ -1,13 +1,27 @@
{
"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": {
"code": "Code (utilis\u00e9 dans l'interface Home Assistant)",
diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json
index 0562222eea3..955dc5e311e 100644
--- a/homeassistant/components/simplisafe/translations/pl.json
+++ b/homeassistant/components/simplisafe/translations/pl.json
@@ -5,9 +5,15 @@
},
"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": {
+ "reauth_confirm": {
+ "data": {
+ "password": "Has\u0142o"
+ }
+ },
"user": {
"data": {
"code": "Kod (u\u017cywany w interfejsie Home Assistanta)",
diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py
index a5c6681eb2b..94f64a4eb43 100644
--- a/homeassistant/components/skybell/binary_sensor.py
+++ b/homeassistant/components/skybell/binary_sensor.py
@@ -4,7 +4,12 @@ import logging
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 @@ SCAN_INTERVAL = timedelta(seconds=5)
# 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/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py
index 39ae3e7c658..cfbd6f576be 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 @@ class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity):
@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 ecc00f12370..49d21f2b2c1 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 @@ class SmappeePresence(BinarySensorEntity):
@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 @@ class SmappeePresence(BinarySensorEntity):
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/strings.json b/homeassistant/components/smappee/strings.json
index 1bec8fda0cc..63cf4254e54 100644
--- a/homeassistant/components/smappee/strings.json
+++ b/homeassistant/components/smappee/strings.json
@@ -19,7 +19,7 @@
"title": "Discovered Smappee device"
},
"pick_implementation": {
- "title": "Pick Authentication Method"
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json
index 54a781c1f4a..ee905f0ca84 100644
--- a/homeassistant/components/smappee/translations/ca.json
+++ b/homeassistant/components/smappee/translations/ca.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Temps d'espera esgotat generant l'URL d'autoritzaci\u00f3.",
"connection_error": "No s'ha pogut connectar amb el Smappee.",
"invalid_mdns": "Dispositiu no compatible amb la integraci\u00f3 de Smappee.",
- "missing_configuration": "El component 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})"
},
"flow_title": "Smappee: {name}",
"step": {
@@ -23,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 00000000000..0e77c8fbd7a
--- /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/en.json b/homeassistant/components/smappee/translations/en.json
index 57f1498d5fa..a6cd10d0806 100644
--- a/homeassistant/components/smappee/translations/en.json
+++ b/homeassistant/components/smappee/translations/en.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Timeout generating authorize url.",
"connection_error": "Failed to connect to Smappee device.",
"invalid_mdns": "Unsupported device for the Smappee integration.",
- "missing_configuration": "The 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})"
},
"flow_title": "Smappee: {name}",
"step": {
diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json
index 543c988b356..5d65081f9f6 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/fr.json b/homeassistant/components/smappee/translations/fr.json
index 18985ce9f34..4bbed6615ca 100644
--- a/homeassistant/components/smappee/translations/fr.json
+++ b/homeassistant/components/smappee/translations/fr.json
@@ -1,17 +1,34 @@
{
"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 00000000000..5bb10e0f851
--- /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 66994517c2f..3a2cc535c6d 100644
--- a/homeassistant/components/smappee/translations/it.json
+++ b/homeassistant/components/smappee/translations/it.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"connection_error": "Impossibile connettersi al dispositivo Smappee.",
"invalid_mdns": "Dispositivo non supportato per l'integrazione Smappee.",
- "missing_configuration": "Il componente 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})"
},
"flow_title": "Smappee: {name}",
"step": {
@@ -23,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 05557a6046d..b3e37ee6d01 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 7f514644918..90dd0b10486 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 76e96614aa6..11b8b1bdd30 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 da5a481c22b..921f80323f9 100644
--- a/homeassistant/components/smappee/translations/pl.json
+++ b/homeassistant/components/smappee/translations/pl.json
@@ -1,10 +1,21 @@
{
"config": {
"abort": {
+ "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.",
"missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
},
"step": {
+ "environment": {
+ "data": {
+ "environment": "\u015arodowisko"
+ }
+ },
+ "local": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ }
+ },
"pick_implementation": {
"title": "Wybierz metod\u0119 uwierzytelniania"
}
diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json
index b3650a483f6..37434488e3d 100644
--- a/homeassistant/components/smappee/translations/ru.json
+++ b/homeassistant/components/smappee/translations/ru.json
@@ -6,7 +6,8 @@
"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.",
"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.",
"invalid_mdns": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.",
- "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."
+ "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."
},
"flow_title": "Smappee: {name}",
"step": {
diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json
index 7636ea5b34b..5374c535a12 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 00000000000..936e9817d92
--- /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 00000000000..3b2d79a34a7
--- /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 00000000000..a40ab60cd1e
--- /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 00000000000..8a08a06c699
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py
index 2d22841660a..c826b5d8f4d 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 f0a1df88695..a277388c140 100644
--- a/homeassistant/components/smarthab/config_flow.py
+++ b/homeassistant/components/smarthab/config_flow.py
@@ -16,6 +16,9 @@ _LOGGER = logging.getLogger(__name__)
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 @@ class SmartHabConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 7279eb6ca79..3d366edbf73 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 974cde35faf..d184a3ca6ce 100644
--- a/homeassistant/components/smartthings/__init__.py
+++ b/homeassistant/components/smartthings/__init__.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
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 async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
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 825cf149952..41e915d5c95 100644
--- a/homeassistant/components/smartthings/binary_sensor.py
+++ b/homeassistant/components/smartthings/binary_sensor.py
@@ -3,7 +3,16 @@ from typing import Optional, Sequence
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_TO_ATTRIB = {
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 58ea833cb7d..30ef278d1d1 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 7de3b98b1da..f0240886913 100644
--- a/homeassistant/components/smartthings/sensor.py
+++ b/homeassistant/components/smartthings/sensor.py
@@ -5,6 +5,7 @@ from typing import Optional, Sequence
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 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
ENERGY_KILO_WATT_HOUR,
+ LIGHT_LUX,
MASS_KILOGRAMS,
PERCENTAGE,
POWER_WATT,
@@ -41,7 +43,12 @@ CAPABILITY_TO_SENSORS = {
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_TO_SENSORS = {
)
],
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 00000000000..91299004ed3
--- /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 c355c437689..6051cbbabce 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 f8b9114ae0e..965102f07f7 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/translations/et.json b/homeassistant/components/smhi/translations/et.json
new file mode 100644
index 00000000000..984b43015d7
--- /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/sms/translations/de.json b/homeassistant/components/sms/translations/de.json
new file mode 100644
index 00000000000..273daf6ef0a
--- /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 25c08a1e7fe..b4c479cfd50 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 00000000000..3b2d79a34a7
--- /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 eec34cc0197..bfe331ee89e 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 31eb0491eb4..4e65b60280b 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 2b085d1ba40..0cd498b4e3b 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -149,7 +149,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensor):
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 @@ class SolarEdgeInventorySensor(SolarEdgeSensor):
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):
diff --git a/homeassistant/components/solarlog/translations/pl.json b/homeassistant/components/solarlog/translations/pl.json
index 6769d51c2c2..1577982d3d7 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/solax/manifest.json b/homeassistant/components/solax/manifest.json
index 5d8590389d8..bf2d3d72cc5 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/no.json b/homeassistant/components/soma/translations/no.json
index 5a084433eed..5c2c01ca7a6 100644
--- a/homeassistant/components/soma/translations/no.json
+++ b/homeassistant/components/soma/translations/no.json
@@ -13,9 +13,11 @@
"step": {
"user": {
"data": {
- "host": "Vert"
+ "host": "Vert",
+ "port": ""
},
- "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect."
+ "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index fbc76e7c938..c3aa3a58837 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -28,7 +28,7 @@ DEVICES = "devices"
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(seconds=30)
+SCAN_INTERVAL = timedelta(minutes=1)
CONF_OPTIMISTIC = "optimistic"
diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py
index 2d143fbd196..80fc2192d8e 100644
--- a/homeassistant/components/somfy/config_flow.py
+++ b/homeassistant/components/somfy/config_flow.py
@@ -24,6 +24,6 @@ class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
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 d1fa921bb8e..384b9c3c5e5 100644
--- a/homeassistant/components/somfy/strings.json
+++ b/homeassistant/components/somfy/strings.json
@@ -1,14 +1,16 @@
{
"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.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
"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 d3e2f7b16aa..40af5096345 100644
--- a/homeassistant/components/somfy/translations/ca.json
+++ b/homeassistant/components/somfy/translations/ca.json
@@ -3,10 +3,12 @@
"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."
+ "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})",
+ "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/en.json b/homeassistant/components/somfy/translations/en.json
index da7890f959b..18910be2595 100644
--- a/homeassistant/components/somfy/translations/en.json
+++ b/homeassistant/components/somfy/translations/en.json
@@ -3,10 +3,12 @@
"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."
+ "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})",
+ "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 bbb1cedad98..6d11afcba47 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 5df01fe951b..3214b3a36de 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 7e1a15cbde8..250ca10c5ec 100644
--- a/homeassistant/components/somfy/translations/it.json
+++ b/homeassistant/components/somfy/translations/it.json
@@ -3,14 +3,16 @@
"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."
+ "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})",
+ "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 9748da483bf..43d4ba146b3 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 f34c07efd06..5a7100bbb3f 100644
--- a/homeassistant/components/somfy/translations/lb.json
+++ b/homeassistant/components/somfy/translations/lb.json
@@ -3,7 +3,8 @@
"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})"
},
"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 6f8e3c3b993..fefd863645c 100644
--- a/homeassistant/components/somfy/translations/no.json
+++ b/homeassistant/components/somfy/translations/no.json
@@ -3,10 +3,12 @@
"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."
+ "missing_configuration": "Somfy-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 85292205b28..89659e4e80b 100644
--- a/homeassistant/components/somfy/translations/ru.json
+++ b/homeassistant/components/somfy/translations/ru.json
@@ -3,7 +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."
+ "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.",
+ "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 b5875aaf088..5e41fbbfe2e 100644
--- a/homeassistant/components/somfy/translations/zh-Hant.json
+++ b/homeassistant/components/somfy/translations/zh-Hant.json
@@ -3,10 +3,12 @@
"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"
+ "missing_configuration": "Somfy \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 b849a490940..ac3bf0673f1 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 @@ class SomfyShade(CoverEntity):
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 601509aa575..8cb64bb527a 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 @@ from .const import (
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 ec1a29c660b..753fb829268 100644
--- a/homeassistant/components/sonarr/config_flow.py
+++ b/homeassistant/components/sonarr/config_flow.py
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
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 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
"""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}
- 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")
+ if CONF_VERIFY_SSL not in user_input:
+ user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
- return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
+ 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,
+ )
+
+ 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_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 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
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 3cd6e88913b..65146b90759 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 481a3d381f0..7a50879195d 100644
--- a/homeassistant/components/sonarr/strings.json
+++ b/homeassistant/components/sonarr/strings.json
@@ -13,6 +13,10 @@
"ssl": "Sonarr uses a SSL certificate",
"verify_ssl": "Sonarr uses a proper certificate"
}
+ },
+ "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": "Successfully re-authenticated",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json
index ed59caf89df..77ff62d6f08 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,6 +11,10 @@
},
"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",
diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json
new file mode 100644
index 00000000000..f76b9222a41
--- /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 9e62ea16d77..819fe3e7105 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,6 +11,10 @@
},
"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",
diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json
index 29db7cfbd77..343af035865 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/hu.json b/homeassistant/components/sonarr/translations/hu.json
new file mode 100644
index 00000000000..f5301e874ea
--- /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/lb.json b/homeassistant/components/sonarr/translations/lb.json
index 23c8116498c..554bb3eebd2 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 2a7ede2964a..ae0bec87cbd 100644
--- a/homeassistant/components/sonarr/translations/no.json
+++ b/homeassistant/components/sonarr/translations/no.json
@@ -2,18 +2,25 @@
"config": {
"abort": {
"already_configured": "Tjenesten er allerede konfigurert",
+ "reauth_successful": "Godkjent p\u00e5 nytt",
"unknown": "Uventet feil"
},
"error": {
"cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning"
},
+ "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"
},
@@ -30,5 +37,6 @@
}
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json
index e2c60427b7e..f2c56d3e540 100644
--- a/homeassistant/components/sonarr/translations/pl.json
+++ b/homeassistant/components/sonarr/translations/pl.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "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": {
diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json
index 158a3d59391..7eaf713101a 100644
--- a/homeassistant/components/sonarr/translations/ru.json
+++ b/homeassistant/components/sonarr/translations/ru.json
@@ -2,6 +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.",
+ "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,6 +11,10 @@
},
"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",
diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json
index dc03a007099..4bac445694e 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,6 +11,10 @@
},
"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",
diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json
index a5f52833f4e..5975bb955fa 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/no.json b/homeassistant/components/songpal/translations/no.json
index 1096e9a0e89..eb07eeb2daa 100644
--- a/homeassistant/components/songpal/translations/no.json
+++ b/homeassistant/components/songpal/translations/no.json
@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "Tilkobling mislyktes."
},
+ "flow_title": "",
"step": {
"init": {
"description": "Vil du sette opp {name} ({host})?"
diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json
index cc420f0f83a..6b5d29e06b3 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 efad23ee1f2..daded59cadf 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -9,5 +9,7 @@
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
}
],
- "codeowners": []
+ "codeowners": [
+ "@cgtobi"
+ ]
}
diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py
index 75256b60cfb..1fda59207ec 100644
--- a/homeassistant/components/spc/binary_sensor.py
+++ b/homeassistant/components/spc/binary_sensor.py
@@ -3,7 +3,12 @@ import logging
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 @@ _LOGGER = logging.getLogger(__name__)
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/spider/translations/de.json b/homeassistant/components/spider/translations/de.json
new file mode 100644
index 00000000000..6f398062876
--- /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 807ba246694..8658343db6a 100644
--- a/homeassistant/components/spider/translations/fr.json
+++ b/homeassistant/components/spider/translations/fr.json
@@ -2,6 +2,19 @@
"config": {
"abort": {
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ },
+ "error": {
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "title": "Connectez-vous avec le compte mijn.ithodaalderop.nl"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json
new file mode 100644
index 00000000000..1f08b96ee10
--- /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 00000000000..4d00f0bfc74
--- /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 00000000000..4b9e07cb01f
--- /dev/null
+++ b/homeassistant/components/spider/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py
index bbff510db14..a3ec307d67b 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 @@ from homeassistant.const import (
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 @@ CONFIG_SCHEMA = vol.Schema(
)
-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 = hass_splunk(
+ session=async_get_clientsession(hass),
+ host=host,
+ port=port,
+ token=token,
+ use_ssl=use_ssl,
+ verify_ssl=verify_ssl,
+ )
- event_collector = f"{uri_scheme}{host}:{port}/services/collector/event"
- headers = {AUTHORIZATION: f"Splunk {token}"}
+ if not await event_collector.check(connectivity=False, token=True, busy=False):
+ return False
- def splunk_event_listener(event):
+ 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 setup(hass, config):
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)
+ 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.listen(EVENT_STATE_CHANGED, splunk_event_listener)
+ 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 337458b4c3f..d51d6c712de 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 d17cd43c47f..db2f35ded91 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/strings.json b/homeassistant/components/spotify/strings.json
index 8e3fa6fc679..5a7e56013cc 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -1,7 +1,7 @@
{
"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}"
diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json
index 005e0c5c331..db47a10ed24 100644
--- a/homeassistant/components/spotify/translations/ca.json
+++ b/homeassistant/components/spotify/translations/ca.json
@@ -4,6 +4,7 @@
"already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Spotify.",
"authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"missing_configuration": "La integraci\u00f3 Spotify no est\u00e0 configurada. 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})",
"reauth_account_mismatch": "El compte Spotify autenticat, no coincideix amb el compte necessari per a la re-autenticaci\u00f3."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json
index 6cdafbf061c..c2f1b5544ae 100644
--- a/homeassistant/components/spotify/translations/en.json
+++ b/homeassistant/components/spotify/translations/en.json
@@ -4,6 +4,7 @@
"already_setup": "You can only configure one Spotify account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json
index 95c29f7a336..025777ad3f6 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 00000000000..059b892f009
--- /dev/null
+++ b/homeassistant/components/spotify/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Saate konfigureerida ainult \u00fche Spotify konto.",
+ "authorize_url_timeout": "Kinnituse URLi ajal\u00f5pp",
+ "missing_configuration": "Spotify sidumine pole seadistatud. Palun j\u00e4rgige dokumentatsiooni."
+ },
+ "create_entry": {
+ "default": "Edukalt Spotifyga autenditud."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Valige autentimismeetod"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json
index 629aa5c681f..251c85920aa 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/it.json b/homeassistant/components/spotify/translations/it.json
index bb494f27860..648f071b8fe 100644
--- a/homeassistant/components/spotify/translations/it.json
+++ b/homeassistant/components/spotify/translations/it.json
@@ -4,6 +4,7 @@
"already_setup": "\u00c8 possibile configurare un solo account di Spotify.",
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione",
"missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
"reauth_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json
index deb55479c1e..37dccd8c1a6 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 fedaff526a6..59aaf936dc1 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 82b25512001..b300066ca95 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 c2e151e5eb7..6ab60ddbc0c 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/ru.json b/homeassistant/components/spotify/translations/ru.json
index c496fa389d9..82190aa57bb 100644
--- a/homeassistant/components/spotify/translations/ru.json
+++ b/homeassistant/components/spotify/translations/ru.json
@@ -4,6 +4,7 @@
"already_setup": "\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.",
"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": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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.",
"reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0435\u0439 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"create_entry": {
diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json
index a4fca36205f..d7e52012c36 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/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json
index 4d024c59d26..8671822cdf8 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 8107b902b4b..6119bc34f8d 100644
--- a/homeassistant/components/squeezebox/translations/fr.json
+++ b/homeassistant/components/squeezebox/translations/fr.json
@@ -1,6 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "no_server_found": "Aucun serveur LMS trouv\u00e9."
+ },
"error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
"no_server_found": "Impossible de d\u00e9couvrir automatiquement le serveur.",
"unknown": "Erreur inattendue"
},
@@ -12,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 00000000000..3b2d79a34a7
--- /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 00000000000..bb140f4ca89
--- /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/no.json b/homeassistant/components/squeezebox/translations/no.json
index dd9192cfb50..ddda0b61be2 100644
--- a/homeassistant/components/squeezebox/translations/no.json
+++ b/homeassistant/components/squeezebox/translations/no.json
@@ -10,11 +10,13 @@
"no_server_found": "Kan ikke automatisk oppdage serveren.",
"unknown": "Uventet feil"
},
+ "flow_title": "",
"step": {
"edit": {
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"title": "Redigere tilkoblingsinformasjon"
@@ -26,5 +28,6 @@
"title": "Konfigurer Logitech Media Server"
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/pl.json b/homeassistant/components/squeezebox/translations/pl.json
index a4339711918..4bf27bbb3ec 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 555d68cd5d4..af2ae21dac3 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -17,16 +17,22 @@ SCAN_INTERVAL = timedelta(seconds=60)
# 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 @@ class Scanner:
"""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 00000000000..7e3c4103ccf
--- /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/starline/translations/no.json b/homeassistant/components/starline/translations/no.json
index 89dc882cf82..36545f3efd7 100644
--- a/homeassistant/components/starline/translations/no.json
+++ b/homeassistant/components/starline/translations/no.json
@@ -17,7 +17,9 @@
"auth_captcha": {
"data": {
"captcha_code": "Kode fra bilde"
- }
+ },
+ "description": "",
+ "title": ""
},
"auth_mfa": {
"data": {
diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py
index 91c4018d899..b986cddaf68 100644
--- a/homeassistant/components/stream/const.py
+++ b/homeassistant/components/stream/const.py
@@ -19,3 +19,4 @@ MAX_SEGMENTS = 3 # Max number of segments to keep around
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 5e4e85ceea6..20931abf11e 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -84,7 +84,7 @@ class StreamOutput:
"""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/worker.py b/homeassistant/components/stream/worker.py
index 9e036a764f8..22f67432a1b 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -6,7 +6,7 @@ import time
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__)
@@ -200,6 +200,12 @@ def _stream_worker_internal(hass, stream, quit_event):
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
@@ -222,15 +228,26 @@ def _stream_worker_internal(hass, stream, quit_event):
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/switch/group.py b/homeassistant/components/switch/group.py
new file mode 100644
index 00000000000..1636054663d
--- /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 8b751871014..c9dd3553928 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 @@ class LightSwitch(LightEntity):
"""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/es-419.json b/homeassistant/components/switch/translations/es-419.json
index a7087a1bbf1..7fb04127b15 100644
--- a/homeassistant/components/switch/translations/es-419.json
+++ b/homeassistant/components/switch/translations/es-419.json
@@ -14,5 +14,11 @@
"turned_on": "{entity_name} encendido"
}
},
+ "state": {
+ "_": {
+ "off": "",
+ "on": ""
+ }
+ },
"title": "Interruptor"
}
\ No newline at end of file
diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json
index d992df0421f..d68938ddda0 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 7ac96bd7039..bee9eb957d5 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/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json
index 6ce2ab3d29a..e286f5f138a 100644
--- a/homeassistant/components/switch/translations/zh-Hant.json
+++ b/homeassistant/components/switch/translations/zh-Hant.json
@@ -6,8 +6,8 @@
"turn_on": "\u958b\u555f{entity_name}"
},
"condition_type": {
- "is_off": "{entity_name}\u5df2\u95dc\u9589",
- "is_on": "{entity_name}\u5df2\u958b\u555f"
+ "is_off": "{entity_name}\u70ba\u95dc\u9589",
+ "is_on": "{entity_name}j\u70ba\u958b\u555f"
},
"trigger_type": {
"turned_off": "{entity_name}\u5df2\u95dc\u9589",
diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json
index 76eaf5f21f3..8e568131e62 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 00000000000..3b2d79a34a7
--- /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 bd174d000c8..2c092f86003 100644
--- a/homeassistant/components/syncthru/translations/pl.json
+++ b/homeassistant/components/syncthru/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": {
"invalid_url": "Nieprawid\u0142owy URL",
diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py
index 5a619c821dc..4417f72918d 100644
--- a/homeassistant/components/synology/camera.py
+++ b/homeassistant/components/synology/camera.py
@@ -42,6 +42,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
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 c8f665cb408..df43c5668f3 100644
--- a/homeassistant/components/synology_chat/notify.py
+++ b/homeassistant/components/synology_chat/notify.py
@@ -10,7 +10,7 @@ from homeassistant.components.notify import (
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 @@ class SynologyChatNotificationService(BaseNotificationService):
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 2e4b550337b..7f6cef46cbc 100644
--- a/homeassistant/components/synology_dsm/__init__.py
+++ b/homeassistant/components/synology_dsm/__init__.py
@@ -10,6 +10,7 @@ from synology_dsm.api.core.utilization import SynoCoreUtilization
from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.dsm.network import SynoDSMNetwork
from synology_dsm.api.storage.storage import SynoStorage
+from synology_dsm.api.surveillance_station import SynoSurveillanceStation
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -22,6 +23,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import callback
@@ -225,12 +227,15 @@ class SynoApi:
self.security: SynoCoreSecurity = None
self.storage: SynoStorage = None
self.utilisation: SynoCoreUtilization = None
+ self.surveillance_station: SynoSurveillanceStation = None
# Should we fetch them
self._fetching_entities = {}
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
@@ -247,9 +252,15 @@ class SynoApi:
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"),
)
+ await self._hass.async_add_executor_job(self.dsm.discover_apis)
+ self._with_surveillance_station = bool(
+ self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY)
+ )
+
self._async_setup_api_requests()
await self._hass.async_add_executor_job(self._fetch_device_configuration)
@@ -294,8 +305,14 @@ class SynoApi:
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
@@ -308,10 +325,16 @@ class SynoApi:
self.dsm.reset(self.utilisation)
self.utilisation = None
+ if not self._with_surveillance_station:
+ self.dsm.reset(self.surveillance_station)
+ self.surveillance_station = None
+
def _fetch_device_configuration(self):
"""Fetch initial device config."""
self.information = self.dsm.information
+ self.information.update()
self.network = self.dsm.network
+ self.network.update()
if self._with_security:
self.security = self.dsm.security
@@ -322,6 +345,9 @@ class SynoApi:
if self._with_utilisation:
self.utilisation = self.dsm.utilisation
+ if self._with_surveillance_station:
+ self.surveillance_station = self.dsm.surveillance_station
+
async def async_unload(self):
"""Stop interacting with the NAS and prepare for removal from hass."""
self._unsub_dispatcher()
@@ -329,7 +355,7 @@ class SynoApi:
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)
@@ -343,6 +369,8 @@ class SynologyDSMEntity(Entity):
entity_info: Dict[str, str],
):
"""Initialize the Synology DSM entity."""
+ super().__init__()
+
self._api = api
self._api_key = entity_type.split(":")[0]
self.entity_type = entity_type.split(":")[-1]
@@ -444,7 +472,7 @@ class SynologyDSMDeviceEntity(SynologyDSMEntity):
self._device_type = None
if "volume" in entity_type:
- volume = self._api.storage._get_volume(self._device_id)
+ volume = self._api.storage.get_volume(self._device_id)
# Volume does not have a name
self._device_name = volume["id"].replace("_", " ").capitalize()
self._device_manufacturer = "Synology"
@@ -457,7 +485,7 @@ class SynologyDSMDeviceEntity(SynologyDSMEntity):
.replace("shr", "SHR")
)
elif "disk" in entity_type:
- disk = self._api.storage._get_disk(self._device_id)
+ disk = self._api.storage.get_disk(self._device_id)
self._device_name = disk["name"]
self._device_manufacturer = disk["vendor"]
self._device_model = disk["model"].strip()
diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py
index c95b4298f5d..a75f57db678 100644
--- a/homeassistant/components/synology_dsm/binary_sensor.py
+++ b/homeassistant/components/synology_dsm/binary_sensor.py
@@ -1,10 +1,7 @@
"""Support for Synology DSM binary sensors."""
from typing import Dict
-from homeassistant.components.binary_sensor import (
- DEVICE_CLASS_SAFETY,
- BinarySensorEntity,
-)
+from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DISKS
from homeassistant.helpers.typing import HomeAssistantType
@@ -17,8 +14,6 @@ from .const import (
SYNO_API,
)
-DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY
-
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
@@ -76,8 +71,3 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
if attr is None:
return None
return attr
-
- @property
- def device_class(self):
- """Return the device class of this binary sensor."""
- return DEFAULT_DEVICE_CLASS
diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py
new file mode 100644
index 00000000000..80e6802e443
--- /dev/null
+++ b/homeassistant/components/synology_dsm/camera.py
@@ -0,0 +1,101 @@
+"""Support for Synology DSM cameras."""
+from typing import Dict
+
+from synology_dsm.api.surveillance_station import SynoSurveillanceStation
+
+from homeassistant.components.camera import SUPPORT_STREAM, Camera
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import SynologyDSMEntity
+from .const import (
+ DOMAIN,
+ ENTITY_CLASS,
+ ENTITY_ENABLE,
+ ENTITY_ICON,
+ ENTITY_NAME,
+ ENTITY_UNIT,
+ SYNO_API,
+)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Synology NAS binary sensor."""
+
+ api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+
+ if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
+ return
+
+ surveillance_station = api.surveillance_station
+ await hass.async_add_executor_job(surveillance_station.update)
+ cameras = surveillance_station.get_all_cameras()
+ entities = [SynoDSMCamera(api, camera) for camera in cameras]
+
+ async_add_entities(entities)
+
+
+class SynoDSMCamera(SynologyDSMEntity, Camera):
+ """Representation a Synology camera."""
+
+ def __init__(self, api, camera):
+ """Initialize a Synology camera."""
+ super().__init__(
+ api,
+ f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera.id}",
+ {
+ ENTITY_NAME: camera.name,
+ ENTITY_CLASS: None,
+ ENTITY_ICON: None,
+ ENTITY_ENABLE: True,
+ ENTITY_UNIT: None,
+ },
+ )
+ self._camera = camera
+
+ @property
+ def device_info(self) -> Dict[str, any]:
+ """Return the device information."""
+ return {
+ "identifiers": {(DOMAIN, self._api.information.serial, self._camera.id)},
+ "name": self._camera.name,
+ "model": self._camera.model,
+ "via_device": (
+ DOMAIN,
+ self._api.information.serial,
+ SynoSurveillanceStation.INFO_API_KEY,
+ ),
+ }
+
+ @property
+ def supported_features(self) -> int:
+ """Return supported features of this camera."""
+ return SUPPORT_STREAM
+
+ @property
+ def is_recording(self):
+ """Return true if the device is recording."""
+ return self._camera.is_recording
+
+ @property
+ def motion_detection_enabled(self):
+ """Return the camera motion detection status."""
+ return self._camera.is_motion_detection_enabled
+
+ def camera_image(self) -> bytes:
+ """Return bytes of camera image."""
+ return self._api.surveillance_station.get_camera_image(self._camera.id)
+
+ async def stream_source(self) -> str:
+ """Return the source of the stream."""
+ return self._camera.live_view.rtsp
+
+ def enable_motion_detection(self):
+ """Enable motion detection in the camera."""
+ self._api.surveillance_station.enable_motion_detection(self._camera.id)
+
+ def disable_motion_detection(self):
+ """Disable motion detection in camera."""
+ self._api.surveillance_station.disable_motion_detection(self._camera.id)
diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py
index a4d7d75e073..dd4b7cf7e10 100644
--- a/homeassistant/components/synology_dsm/config_flow.py
+++ b/homeassistant/components/synology_dsm/config_flow.py
@@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import callback
@@ -34,6 +35,7 @@ from .const import (
DEFAULT_PORT_SSL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SSL,
+ DEFAULT_TIMEOUT,
)
from .const import DOMAIN # pylint: disable=unused-import
@@ -124,7 +126,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
else:
port = DEFAULT_PORT
- api = SynologyDSM(host, port, username, password, use_ssl)
+ api = SynologyDSM(host, port, username, password, use_ssl, timeout=30)
try:
serial = await self.hass.async_add_executor_job(
@@ -250,7 +252,13 @@ class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow):
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)
@@ -260,14 +268,15 @@ def _login_and_fetch_syno_info(api, otp_code):
"""Login to the NAS and fetch basic data."""
# These do i/o
api.login(otp_code)
- utilisation = api.utilisation
- storage = api.storage
+ api.utilisation.update()
+ api.storage.update()
+ api.network.update()
if (
not api.information.serial
- or utilisation.cpu_user_load is None
- or not storage.disks_ids
- or not storage.volumes_ids
+ or api.utilisation.cpu_user_load is None
+ or not api.storage.disks_ids
+ or not api.storage.volumes_ids
or not api.network.macs
):
raise InvalidData
diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py
index 163561d13a0..82bb232461e 100644
--- a/homeassistant/components/synology_dsm/const.py
+++ b/homeassistant/components/synology_dsm/const.py
@@ -2,17 +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", "sensor"]
+PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"]
# Entry keys
SYNO_API = "syno_api"
@@ -26,6 +31,7 @@ DEFAULT_PORT = 5000
DEFAULT_PORT_SSL = 5001
# Options
DEFAULT_SCAN_INTERVAL = 15 # min
+DEFAULT_TIMEOUT = 10 # sec
ENTITY_NAME = "name"
@@ -37,29 +43,29 @@ ENTITY_ENABLE = "enable"
# Entity keys should start with the API_KEY to fetch
# Binary sensors
+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,
+ },
+}
+
STORAGE_DISK_BINARY_SENSORS = {
f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": {
ENTITY_NAME: "Exceeded Max Bad Sectors",
ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:test-tube",
- ENTITY_CLASS: 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",
ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:test-tube",
- ENTITY_CLASS: None,
- ENTITY_ENABLE: True,
- },
-}
-
-SECURITY_BINARY_SENSORS = {
- f"{SynoCoreSecurity.API_KEY}:status": {
- ENTITY_NAME: "Security status",
- ENTITY_UNIT: None,
- ENTITY_ICON: "mdi:checkbox-marked-circle-outline",
- ENTITY_CLASS: "safety",
+ ENTITY_ICON: None,
+ ENTITY_CLASS: DEVICE_CLASS_SAFETY,
ENTITY_ENABLE: True,
},
}
@@ -211,15 +217,15 @@ STORAGE_VOL_SENSORS = {
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,
},
}
@@ -241,11 +247,44 @@ STORAGE_DISK_SENSORS = {
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/manifest.json b/homeassistant/components/synology_dsm/manifest.json
index 6ad926cfb9e..aee8c464464 100644
--- a/homeassistant/components/synology_dsm/manifest.json
+++ b/homeassistant/components/synology_dsm/manifest.json
@@ -2,8 +2,8 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
- "requirements": ["python-synology==0.8.2"],
- "codeowners": ["@ProtoThis", "@Quentame"],
+ "requirements": ["python-synology==0.9.0"],
+ "codeowners": ["@hacf-fr", "@Quentame"],
"config_flow": true,
"ssdp": [
{
diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py
index 22171fdf2f5..31013451682 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.const import (
)
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 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity):
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 c46f645719f..2bb81f6711d 100644
--- a/homeassistant/components/synology_dsm/strings.json
+++ b/homeassistant/components/synology_dsm/strings.json
@@ -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/switch.py b/homeassistant/components/synology_dsm/switch.py
new file mode 100644
index 00000000000..ee29c9f2692
--- /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 fef0bca2ce4..e265bc31da7 100644
--- a/homeassistant/components/synology_dsm/translations/ca.json
+++ b/homeassistant/components/synology_dsm/translations/ca.json
@@ -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 57dc028c0f4..d1f593c7938 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 00000000000..e23b1fe0cf6
--- /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 48a8118528a..fc78d3cfa66 100644
--- a/homeassistant/components/synology_dsm/translations/en.json
+++ b/homeassistant/components/synology_dsm/translations/en.json
@@ -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 a498333e049..ca02cf41e23 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/fr.json b/homeassistant/components/synology_dsm/translations/fr.json
index 6c8b627a76b..1c411591f1a 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 1bb6dc026d8..933f9760b5b 100644
--- a/homeassistant/components/synology_dsm/translations/it.json
+++ b/homeassistant/components/synology_dsm/translations/it.json
@@ -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 81b7d5f1435..6c0dc98b4ae 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 9ca8d0cdfa5..63e1be3fa2d 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 5798dce567d..ee7c89f7192 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 2bfc824cb76..c9d16a9a6e8 100644
--- a/homeassistant/components/synology_dsm/translations/no.json
+++ b/homeassistant/components/synology_dsm/translations/no.json
@@ -21,18 +21,22 @@
"link": {
"data": {
"password": "Passord",
+ "port": "",
"ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en",
"username": "Brukernavn"
},
- "description": "Vil du konfigurere {name} ({host})?"
+ "description": "Vil du konfigurere {name} ({host})?",
+ "title": ""
},
"user": {
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en",
"username": "Brukernavn"
- }
+ },
+ "title": ""
}
}
},
@@ -40,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/ru.json b/homeassistant/components/synology_dsm/translations/ru.json
index c0e5da7a1f1..b332afd131f 100644
--- a/homeassistant/components/synology_dsm/translations/ru.json
+++ b/homeassistant/components/synology_dsm/translations/ru.json
@@ -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 6aaee8b44aa..690e622ecd2 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 eec37c812f8..96f8d5283fd 100644
--- a/homeassistant/components/synology_dsm/translations/zh-Hant.json
+++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json
@@ -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 b8d6b1664ac..bb255ba8bf3 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 @@ async def async_setup(hass, config):
"""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 e8ff20b2b5a..e29081257e0 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -35,43 +35,78 @@ if sys.maxsize > 2 ** 32:
else:
CPU_ICON = "mdi:cpu-32-bit"
+# Schema: [name, unit of measurement, icon, device class, flag if mandatory arg]
SENSOR_TYPES = {
- "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None],
- "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None],
- "disk_use_percent": ["Disk use (percent)", PERCENTAGE, "mdi:harddisk", None],
- "ipv4_address": ["IPv4 address", "", "mdi:server-network", None],
- "ipv6_address": ["IPv6 address", "", "mdi:server-network", None],
- "last_boot": ["Last boot", "", "mdi:clock", "timestamp"],
- "load_15m": ["Load (15m)", " ", "mdi:memory", None],
- "load_1m": ["Load (1m)", " ", "mdi:memory", None],
- "load_5m": ["Load (5m)", " ", "mdi:memory", None],
- "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None],
- "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None],
- "memory_use_percent": ["Memory use (percent)", PERCENTAGE, "mdi:memory", None],
- "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None],
- "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None],
- "packets_in": ["Packets in", " ", "mdi:server-network", None],
- "packets_out": ["Packets out", " ", "mdi:server-network", None],
+ "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False],
+ "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False],
+ "disk_use_percent": [
+ "Disk use (percent)",
+ PERCENTAGE,
+ "mdi:harddisk",
+ None,
+ False,
+ ],
+ "ipv4_address": ["IPv4 address", "", "mdi:server-network", None, True],
+ "ipv6_address": ["IPv6 address", "", "mdi:server-network", None, True],
+ "last_boot": ["Last boot", "", "mdi:clock", "timestamp", False],
+ "load_15m": ["Load (15m)", " ", CPU_ICON, None, False],
+ "load_1m": ["Load (1m)", " ", CPU_ICON, None, False],
+ "load_5m": ["Load (5m)", " ", CPU_ICON, None, False],
+ "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None, False],
+ "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None, False],
+ "memory_use_percent": [
+ "Memory use (percent)",
+ PERCENTAGE,
+ "mdi:memory",
+ None,
+ False,
+ ],
+ "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None, True],
+ "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None, True],
+ "packets_in": ["Packets in", " ", "mdi:server-network", None, True],
+ "packets_out": ["Packets out", " ", "mdi:server-network", None, True],
"throughput_network_in": [
"Network throughput in",
DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
None,
+ True,
],
"throughput_network_out": [
"Network throughput out",
DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
- None,
+ True,
],
- "process": ["Process", " ", CPU_ICON, None],
- "processor_use": ["Processor use", PERCENTAGE, CPU_ICON, None],
- "processor_temperature": ["Processor temperature", TEMP_CELSIUS, CPU_ICON, None],
- "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None],
- "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None],
- "swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None],
+ "process": ["Process", " ", CPU_ICON, None, True],
+ "processor_use": ["Processor use", PERCENTAGE, CPU_ICON, None, False],
+ "processor_temperature": [
+ "Processor temperature",
+ TEMP_CELSIUS,
+ CPU_ICON,
+ None,
+ False,
+ ],
+ "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],
}
+
+def check_required_arg(value):
+ """Validate that the required "arg" for the sensor types that need it are set."""
+ for sensor in value:
+ sensor_type = sensor[CONF_TYPE]
+ sensor_arg = sensor.get(CONF_ARG)
+
+ if sensor_arg is None and SENSOR_TYPES[sensor_type][4]:
+ raise vol.RequiredFieldInvalid(
+ f"Mandatory 'arg' is missing for sensor type '{sensor_type}'."
+ )
+
+ return value
+
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_RESOURCES, default={CONF_TYPE: "disk_use"}): vol.All(
@@ -84,6 +119,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
],
+ check_required_arg,
)
}
)
diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json
index 18196a4bf13..0ebbe4054a1 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/pl.json b/homeassistant/components/tado/translations/pl.json
index f95374e0329..46c78ce438e 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/tag/manifest.json b/homeassistant/components/tag/manifest.json
index d330fdaf3f8..f2d3a4133b8 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 00000000000..8cee64dc465
--- /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 00000000000..fdac700612d
--- /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 39e492601bd..af06bf5ca4c 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 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorEntity):
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 7b28989ad8e..fb1129cfa0e 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 @@ class TahomaSensor(TahomaDevice, Entity):
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 b0dd3368ad3..6985072b065 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 @@ class FuelPriceSensor(CoordinatorEntity):
@property
def unit_of_measurement(self):
"""Return unit of measurement."""
- return "€"
+ return CURRENCY_EURO
@property
def state(self):
diff --git a/homeassistant/components/teksavvy/__init__.py b/homeassistant/components/teksavvy/__init__.py
deleted file mode 100644
index ee0dcd1c810..00000000000
--- 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 e114efdce9f..00000000000
--- 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 4ff2bc84dbe..00000000000
--- 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/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index c8f27a9412a..e322481813a 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -6,6 +6,8 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
SPEED_METERS_PER_SECOND,
@@ -40,14 +42,19 @@ SENSOR_TYPES = {
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 8145717b40e..13054967365 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 dc9e5ead1d0..fe8b6568c1e 100644
--- a/homeassistant/components/template/cover.py
+++ b/homeassistant/components/template/cover.py
@@ -29,7 +29,9 @@ from homeassistant.const import (
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 .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
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 632eeea8926..49b0edfab02 100644
--- a/homeassistant/components/template/template_entity.py
+++ b/homeassistant/components/template/template_entity.py
@@ -213,7 +213,6 @@ class TemplateEntity(Entity):
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)
@@ -235,9 +234,7 @@ class TemplateEntity(Entity):
else:
self._self_ref_update_count = 0
- # If we need to make this less sensitive in the future,
- # change the '>=' to a '>' here.
- if self._self_ref_update_count >= len(self._template_attrs):
+ 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]",
@@ -252,22 +249,21 @@ class TemplateEntity(Entity):
event, update.template, update.last_result, update.result
)
- 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()
- self.async_write_ha_state()
self._async_update = result_info.async_refresh
+ 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/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index 2f1c391094c..a8739a86d70 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -6,7 +6,7 @@
"tensorflow==2.3.0",
"tf-models-official==2.3.0",
"pycocotools==2.0.1",
- "numpy==1.19.1",
+ "numpy==1.19.2",
"pillow==7.2.0"
],
"codeowners": []
diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py
index 9e46a30972f..f21c8c76e23 100644
--- a/homeassistant/components/tesla/config_flow.py
+++ b/homeassistant/components/tesla/config_flow.py
@@ -11,6 +11,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
)
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
@@ -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/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py
index 35a16d30f32..3555816213a 100644
--- a/homeassistant/components/thethingsnetwork/sensor.py
+++ b/homeassistant/components/thethingsnetwork/sensor.py
@@ -8,7 +8,7 @@ import async_timeout
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 @@ class TtnDataStorage:
_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 36f4002949b..74f61f17d29 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.14.0"],
+ "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 939c6d1597d..d2ddd645907 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -157,8 +157,7 @@ class TibberSensorElPrice(TibberSensor):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self):
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/translations/fr.json b/homeassistant/components/tibber/translations/fr.json
index 223c1d44780..24fef7886ca 100644
--- a/homeassistant/components/tibber/translations/fr.json
+++ b/homeassistant/components/tibber/translations/fr.json
@@ -5,6 +5,7 @@
},
"error": {
"connection_error": "Erreur de connexion \u00e0 Tibber",
+ "invalid_access_token": "Jeton d'acc\u00e8s non valide",
"timeout": "D\u00e9lai de connexion \u00e0 Tibber"
},
"step": {
diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json
index 4480fb106de..34e078f5467 100644
--- a/homeassistant/components/tibber/translations/no.json
+++ b/homeassistant/components/tibber/translations/no.json
@@ -13,8 +13,10 @@
"data": {
"access_token": "Tilgangstoken"
},
- "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)"
+ "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)",
+ "title": ""
}
}
- }
+ },
+ "title": ""
}
\ No newline at end of file
diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json
index 8ef96358301..d69572b4a42 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/pt.json b/homeassistant/components/tibber/translations/pt.json
new file mode 100644
index 00000000000..243987422dd
--- /dev/null
+++ b/homeassistant/components/tibber/translations/pt.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "access_token": ""
+ },
+ "title": ""
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json
index b76312d957f..dfc968eb066 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/tof/sensor.py b/homeassistant/components/tof/sensor.py
index 58f50f4899e..d9ad178cab2 100644
--- a/homeassistant/components/tof/sensor.py
+++ b/homeassistant/components/tof/sensor.py
@@ -9,14 +9,12 @@ import voluptuous as vol
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 873da5a7864..ce660a60280 100644
--- a/homeassistant/components/tomato/device_tracker.py
+++ b/homeassistant/components/tomato/device_tracker.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_OK,
+ HTTP_UNAUTHORIZED,
)
import homeassistant.helpers.config_validation as cv
@@ -111,7 +112,7 @@ class TomatoDeviceScanner(DeviceScanner):
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 cec5d4ef851..89c8c242516 100644
--- a/homeassistant/components/toon/translations/ca.json
+++ b/homeassistant/components/toon/translations/ca.json
@@ -5,7 +5,8 @@
"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.",
"missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
- "no_agreements": "Aquest compte no t\u00e9 pantalles Toon."
+ "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})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json
index b15caa77aba..eda1dcb1ee3 100644
--- a/homeassistant/components/toon/translations/en.json
+++ b/homeassistant/components/toon/translations/en.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
- "no_agreements": "This account has no Toon displays."
+ "no_agreements": "This account has no Toon displays.",
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json
index 28e8a1dcb61..b6c6e7ad67d 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 c3384f56319..caeed852d0a 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/it.json b/homeassistant/components/toon/translations/it.json
index ed905ff899f..c7cfb228388 100644
--- a/homeassistant/components/toon/translations/it.json
+++ b/homeassistant/components/toon/translations/it.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione",
"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_agreements": "Questo account non ha display Toon."
+ "no_agreements": "Questo account non ha display Toon.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json
index bebd8bb912e..379058f68d1 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 5d7095d0c85..6491c666738 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 69eabaaf28b..da3ce6d84c7 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 37652c4aee1..49103a77b37 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/ru.json b/homeassistant/components/toon/translations/ru.json
index b5ae66ee54e..4a601de3c28 100644
--- a/homeassistant/components/toon/translations/ru.json
+++ b/homeassistant/components/toon/translations/ru.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \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.",
"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": "\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_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon."
+ "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.",
+ "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."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json
index a6d5227afc6..020938792d6 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/translations/no.json b/homeassistant/components/totalconnect/translations/no.json
index a8d6ac5dc23..c312f98f3d2 100644
--- a/homeassistant/components/totalconnect/translations/no.json
+++ b/homeassistant/components/totalconnect/translations/no.json
@@ -11,7 +11,8 @@
"data": {
"password": "Passord",
"username": "Brukernavn"
- }
+ },
+ "title": ""
}
}
}
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
index 7ecced32341..6c9d1f8b2e8 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/tradfri/translations/et.json b/homeassistant/components/tradfri/translations/et.json
new file mode 100644
index 00000000000..5d0a728407a
--- /dev/null
+++ b/homeassistant/components/tradfri/translations/et.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "auth": {
+ "data": {
+ "host": ""
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py
index 958adfd6915..bb1bad67f82 100644
--- a/homeassistant/components/trafikverket_weatherstation/sensor.py
+++ b/homeassistant/components/trafikverket_weatherstation/sensor.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
DEGREE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
+ LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_METERS_PER_SECOND,
TEMP_CELSIUS,
@@ -97,7 +98,7 @@ SENSOR_TYPES = {
],
"precipitation_amount": [
"Precipitation amount",
- "mm",
+ LENGTH_MILLIMETERS,
"precipitation_amount",
"mdi:cup-water",
None,
diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json
index 66d6caf1b17..a09fbba4e85 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 00000000000..d7f39519aad
--- /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/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json
index d71e2c2c590..48b86516917 100644
--- a/homeassistant/components/transmission/translations/no.json
+++ b/homeassistant/components/transmission/translations/no.json
@@ -14,6 +14,7 @@
"host": "Vert",
"name": "Navn",
"password": "Passord",
+ "port": "",
"username": "Brukernavn"
},
"title": "Oppsett av Transmission-klient"
diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json
index a43c2bb0cce..88e32ce4a46 100644
--- a/homeassistant/components/trend/manifest.json
+++ b/homeassistant/components/trend/manifest.json
@@ -2,7 +2,7 @@
"domain": "trend",
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
- "requirements": ["numpy==1.19.1"],
+ "requirements": ["numpy==1.19.2"],
"codeowners": [],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 2eb12750fe8..7ee4922677c 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -11,7 +11,7 @@ from typing import Dict, Optional
from aiohttp import web
import mutagen
-from mutagen.id3 import TextFrame as ID3Text
+from mutagen.id3 import ID3FileType, TextFrame as ID3Text
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -468,9 +468,14 @@ class SpeechManager:
try:
tts_file = mutagen.File(data_bytes)
if tts_file is not None:
- tts_file["artist"] = ID3Text(encoding=3, text=artist)
- tts_file["album"] = ID3Text(encoding=3, text=album)
- tts_file["title"] = ID3Text(encoding=3, text=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/translations/et.json b/homeassistant/components/tuya/translations/et.json
new file mode 100644
index 00000000000..0086fd641f8
--- /dev/null
+++ b/homeassistant/components/tuya/translations/et.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "auth_failed": "Viga tuvastamisel",
+ "conn_error": "\u00dchendamine eba\u00f5nnestus",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine."
+ },
+ "error": {
+ "auth_failed": "Vigane tuvastamine"
+ },
+ "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/no.json b/homeassistant/components/tuya/translations/no.json
index ca6de86f30b..5681f95d984 100644
--- a/homeassistant/components/tuya/translations/no.json
+++ b/homeassistant/components/tuya/translations/no.json
@@ -17,7 +17,8 @@
"platform": "Appen der kontoen din registreres",
"username": "Brukernavn"
},
- "description": "Skriv inn din Tuya-legitimasjon."
+ "description": "Skriv inn din Tuya-legitimasjon.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json
index 7278806b5f6..d306172fab0 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/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json
index 0ed6471e4fd..a9d3c184495 100644
--- a/homeassistant/components/twentemilieu/translations/no.json
+++ b/homeassistant/components/twentemilieu/translations/no.json
@@ -14,7 +14,8 @@
"house_number": "Husnummer",
"post_code": "Postnummer"
},
- "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din."
+ "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json
index bfa38f9ef8a..e654bbac6a4 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/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index 6115821b000..e40fb30a62c 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -16,6 +16,7 @@ import homeassistant.helpers.config_validation as cv
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -312,7 +313,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
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 803a892647f..42d160f2dea 100644
--- a/homeassistant/components/unifi/const.py
+++ b/homeassistant/components/unifi/const.py
@@ -12,6 +12,7 @@ CONF_SITE_ID = "site"
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_TRACK_WIRED_CLIENTS = "track_wired_clients"
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 7c30a34f58f..6fc5b3d9ed7 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
+ CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
@@ -45,6 +46,7 @@ from .const import (
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 @@ class UniFiController:
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/sensor.py b/homeassistant/components/unifi/sensor.py
index 8fdb0ac1461..59aff09811f 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 @@ LOGGER = logging.getLogger(__name__)
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 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) -> 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@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 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor):
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 95d273278bd..ba0b3952dd0 100644
--- a/homeassistant/components/unifi/strings.json
+++ b/homeassistant/components/unifi/strings.json
@@ -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 de2a8ffc562..8fe38c706ba 100644
--- a/homeassistant/components/unifi/translations/ca.json
+++ b/homeassistant/components/unifi/translations/ca.json
@@ -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 a2adfcce308..c7e1ddc7136 100644
--- a/homeassistant/components/unifi/translations/cs.json
+++ b/homeassistant/components/unifi/translations/cs.json
@@ -49,7 +49,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 00000000000..e6b521c1543
--- /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 691f4fb6b01..72dec2e7709 100644
--- a/homeassistant/components/unifi/translations/en.json
+++ b/homeassistant/components/unifi/translations/en.json
@@ -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 da67cd1bcaf..35b1d3d6e29 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 e92c33c19de..03a872a8f96 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)",
@@ -49,6 +52,7 @@
},
"simple_options": {
"data": {
+ "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau",
"track_clients": "Suivi de clients r\u00e9seaux",
"track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)"
},
@@ -56,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 8a06ca440c5..43fd5b31c23 100644
--- a/homeassistant/components/unifi/translations/it.json
+++ b/homeassistant/components/unifi/translations/it.json
@@ -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 a3d2c8f3b69..94160829bad 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 5f7e7192bd9..992ee8192e3 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 5d64d73d1de..f945e5c4d6d 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 2742ae6a0c5..fa0d29c5a88 100644
--- a/homeassistant/components/unifi/translations/no.json
+++ b/homeassistant/components/unifi/translations/no.json
@@ -13,6 +13,7 @@
"data": {
"host": "Vert",
"password": "Passord",
+ "port": "",
"site": "Nettsted-ID",
"username": "Brukernavn",
"verify_ssl": "Kontroller bruker riktig sertifikat"
@@ -59,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 c062d392911..4aecd4502fa 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": {
diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json
index 550867b682b..8276b41e33b 100644
--- a/homeassistant/components/unifi/translations/ru.json
+++ b/homeassistant/components/unifi/translations/ru.json
@@ -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 7ba51c9b621..bd43b062786 100644
--- a/homeassistant/components/unifi/translations/zh-Hant.json
+++ b/homeassistant/components/unifi/translations/zh-Hant.json
@@ -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/upb/translations/pl.json b/homeassistant/components/upb/translations/pl.json
index fcc6fb8bead..18b1b3a8c78 100644
--- a/homeassistant/components/upb/translations/pl.json
+++ b/homeassistant/components/upb/translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z UPB PIM, spr\u00f3buj ponownie.",
- "unknown": "Nieoczekiwany b\u0142\u0105d."
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 52cada89333..773a872f33f 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 016a5a25017..e7c16ef0df9 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -104,19 +104,10 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""
_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 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_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 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
"""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/translations/et.json b/homeassistant/components/upnp/translations/et.json
new file mode 100644
index 00000000000..76145d6e6e1
--- /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 231dce9a402..e31d8b44b10 100644
--- a/homeassistant/components/uptimerobot/binary_sensor.py
+++ b/homeassistant/components/uptimerobot/binary_sensor.py
@@ -4,7 +4,11 @@ import logging
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 @@ class UptimeRobotBinarySensor(BinarySensorEntity):
@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 5be7dcf9b69..7b55ec4dcd0 100644
--- a/homeassistant/components/utility_meter/const.py
+++ b/homeassistant/components/utility_meter/const.py
@@ -5,10 +5,11 @@ HOURLY = "hourly"
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 8372d8e6b22..54f93422abd 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -24,6 +24,7 @@ import homeassistant.util.dt as dt_util
from .const import (
ATTR_VALUE,
+ BIMONTHLY,
CONF_METER,
CONF_METER_NET_CONSUMPTION,
CONF_METER_OFFSET,
@@ -204,6 +205,12 @@ class UtilityMeterSensor(RestoreEntity):
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 @@ class UtilityMeterSensor(RestoreEntity):
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/vacuum/group.py b/homeassistant/components/vacuum/group.py
new file mode 100644
index 00000000000..0219ecdf795
--- /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/ca.json b/homeassistant/components/vacuum/translations/ca.json
index 5f8f234d808..d98a51a5363 100644
--- a/homeassistant/components/vacuum/translations/ca.json
+++ b/homeassistant/components/vacuum/translations/ca.json
@@ -21,7 +21,7 @@
"idle": "Inactiu",
"off": "OFF",
"on": "ON",
- "paused": "Pausat/da",
+ "paused": "Pausat/ada",
"returning": "Retornant a base"
}
},
diff --git a/homeassistant/components/vacuum/translations/et.json b/homeassistant/components/vacuum/translations/et.json
index 56976340c5b..fbdbe330b83 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 97e3955792c..7a959654525 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 361b5a01175..7d392ab37c5 100644
--- a/homeassistant/components/velbus/config_flow.py
+++ b/homeassistant/components/velbus/config_flow.py
@@ -37,7 +37,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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 455aa98b34c..368c4865bab 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 d5f9d4e7ccf..c2defd782f4 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/vera/__init__.py b/homeassistant/components/vera/__init__.py
index b45716b33d6..fdc8503ed70 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 @@ from homeassistant.const import (
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 @@ CONFIG_SCHEMA = vol.Schema(
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 @@ class VeraDevice(Entity):
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 557874f846a..2e66d38e249 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.core import HomeAssistant
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 9b8601e45d1..0946de4a379 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.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__)
@@ -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 @@ class VeraThermostat(VeraDevice, ClimateEntity):
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 @@ class VeraThermostat(VeraDevice, ClimateEntity):
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 @@ class VeraThermostat(VeraDevice, ClimateEntity):
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 @@ class VeraThermostat(VeraDevice, ClimateEntity):
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 @@ class VeraThermostat(VeraDevice, ClimateEntity):
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 17536bcae69..66a2d6879dd 100644
--- a/homeassistant/components/vera/common.py
+++ b/homeassistant/components/vera/common.py
@@ -5,9 +5,12 @@ from typing import DefaultDict, List, NamedTuple, Set
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 a040e4b96b5..754d2eca542 100644
--- a/homeassistant/components/vera/config_flow.py
+++ b/homeassistant/components/vera/config_flow.py
@@ -8,10 +8,16 @@ from requests.exceptions import RequestException
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 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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 c4f1d0efa3a..34ac7faa669 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 a1f536d9cc1..49b15e91eb2 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.core import HomeAssistant
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 @@ class VeraCover(VeraDevice, CoverEntity):
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 250842f1687..47d2d039d2a 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 @@ from homeassistant.helpers.entity import Entity
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 @@ class VeraLight(VeraDevice, LightEntity):
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 f85beb5ba69..46f8c6f189e 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.core import HomeAssistant
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 @@ class VeraLock(VeraDevice, LockEntity):
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 @@ class VeraLock(VeraDevice, LockEntity):
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/scene.py b/homeassistant/components/vera/scene.py
index 2f3069f5332..8bd4473e1c8 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.core import HomeAssistant
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 @@ class VeraScene(Scene):
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 @@ class VeraScene(Scene):
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 3c4e0974b85..9c3dd097a78 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 @@ class VeraSensor(VeraDevice, Entity):
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 @@ class VeraSensor(VeraDevice, Entity):
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 7b294eddbb9..844d1777f5d 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 0a9a94d6372..9e8360bf673 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.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__)
@@ -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 9cc6d871dd7..e54613cdb78 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 757e299792a..8cd8b0672cf 100644
--- a/homeassistant/components/verisure/__init__.py
+++ b/homeassistant/components/verisure/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
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 @@ class VerisureHub:
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/vesync/fan.py b/homeassistant/components/vesync/fan.py
index 7d395d93a74..7cc3f00e1a0 100644
--- a/homeassistant/components/vesync/fan.py
+++ b/homeassistant/components/vesync/fan.py
@@ -21,8 +21,8 @@ DEV_TYPE_TO_HA = {
"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_setup_entry(hass, config_entry, async_add_entities):
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 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
@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 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
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/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json
index 37774921920..b6790d98d39 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/pl.json b/homeassistant/components/vilfo/translations/pl.json
index 29a8a056361..fb439da9bfe 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/vizio/media_player.py b/homeassistant/components/vizio/media_player.py
index ab5386c151b..ac688241682 100644
--- a/homeassistant/components/vizio/media_player.py
+++ b/homeassistant/components/vizio/media_player.py
@@ -143,7 +143,6 @@ class VizioDevice(MediaPlayerEntity):
) -> None:
"""Initialize Vizio device."""
self._config_entry = config_entry
- self._async_unsub_listeners = []
self._apps_coordinator = apps_coordinator
self._name = name
@@ -312,14 +311,14 @@ class VizioDevice(MediaPlayerEntity):
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 @@ class VizioDevice(MediaPlayerEntity):
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/translations/hu.json b/homeassistant/components/vizio/translations/hu.json
index 469649275e1..9c0b31ca427 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 c56171e9319..310fd765026 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 9d22796ea44..e5bcf0875a3 100644
--- a/homeassistant/components/vizio/translations/pl.json
+++ b/homeassistant/components/vizio/translations/pl.json
@@ -1,11 +1,11 @@
{
"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.",
"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 23d4c9ff864..69029ea7031 100644
--- a/homeassistant/components/voicerss/tts.py
+++ b/homeassistant/components/voicerss/tts.py
@@ -28,32 +28,55 @@ ERROR_MSG = [
]
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
new file mode 100644
index 00000000000..6dee3fb9faf
--- /dev/null
+++ b/homeassistant/components/volumio/translations/fr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "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",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..1708694e123
--- /dev/null
+++ b/homeassistant/components/volumio/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "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 00000000000..f4ec0ecbc26
--- /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 00000000000..4741f8a3b54
--- /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/services.yaml b/homeassistant/components/webostv/services.yaml
index 5719e339793..430916f7c71 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 036cd690da2..11d97f58f50 100644
--- a/homeassistant/components/websocket_api/commands.py
+++ b/homeassistant/components/websocket_api/commands.py
@@ -17,6 +17,7 @@ from homeassistant.exceptions import (
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 handle_subscribe_events(hass, connection, msg):
):
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 handle_subscribe_events(hass, connection, msg):
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 handle_render_template(hass, connection, msg):
)
)
- 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 f01a2880b9d..5f2cfb2257d 100644
--- a/homeassistant/components/websocket_api/const.py
+++ b/homeassistant/components/websocket_api/const.py
@@ -29,6 +29,7 @@ ERR_UNKNOWN_COMMAND = "unknown_command"
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 7c56fcbc606..b71b19d5181 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -11,17 +11,11 @@ from homeassistant.components.http import HomeAssistantView
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 @@ from .const import (
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 @@ class WebSocketHandler:
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 @@ class WebSocketHandler:
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 27d557e8110..52e97b60ccf 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 @@ MINIMAL_MESSAGE_SCHEMA = vol.Schema(
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 bc9755414c6..357d9d95483 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/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json
new file mode 100644
index 00000000000..07d00495af7
--- /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/hu.json b/homeassistant/components/wilight/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..677b104c065
--- /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 00000000000..c04105e0878
--- /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 00000000000..637a81a3f87
--- /dev/null
+++ b/homeassistant/components/wilight/translations/pl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py
index d8967dd064d..77ff464a5bf 100644
--- a/homeassistant/components/wink/binary_sensor.py
+++ b/homeassistant/components/wink/binary_sensor.py
@@ -3,7 +3,16 @@ import logging
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 @@ _LOGGER = logging.getLogger(__name__)
# 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/withings/common.py b/homeassistant/components/withings/common.py
index af81c0e68d3..067133f331f 100644
--- a/homeassistant/components/withings/common.py
+++ b/homeassistant/components/withings/common.py
@@ -29,6 +29,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
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 @@ from .const import Measurement
_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 c9d2d7ca22c..05f6d15ca11 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/ca.json b/homeassistant/components/withings/translations/ca.json
index 88d3ae7e6e6..1731a7bdaaf 100644
--- a/homeassistant/components/withings/translations/ca.json
+++ b/homeassistant/components/withings/translations/ca.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Configuraci\u00f3 de perfil actualitzada.",
"authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
- "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3."
+ "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. 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 Withings."
diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json
index 185bd56153c..53f8a78f3a9 100644
--- a/homeassistant/components/withings/translations/en.json
+++ b/homeassistant/components/withings/translations/en.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Configuration updated for profile.",
"authorize_url_timeout": "Timeout generating authorize url.",
- "missing_configuration": "The Withings integration is not configured. Please follow the documentation."
+ "missing_configuration": "The Withings integration 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 Withings."
diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json
index e59b6e96775..d8b69013bbd 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 7ddb1049abb..a51efff7276 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/it.json b/homeassistant/components/withings/translations/it.json
index 7302797c37c..90824fd0445 100644
--- a/homeassistant/components/withings/translations/it.json
+++ b/homeassistant/components/withings/translations/it.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Configurazione aggiornata per il profilo.",
"authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.",
- "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione."
+ "missing_configuration": "Il componente Withings 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 Withings."
diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json
index 74dacdd2cac..a01672f2227 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 a5102baf917..e3ff23e392a 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 4f382f02a57..a54333ab8f2 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 2b39f8fceab..bdec62a7160 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/ru.json b/homeassistant/components/withings/translations/ru.json
index b26efaedb18..93fcce53d45 100644
--- a/homeassistant/components/withings/translations/ru.json
+++ b/homeassistant/components/withings/translations/ru.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f.",
"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": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Withings \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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."
+ "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Withings \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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."
},
"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/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json
index 3f31c0585f8..02e6d6f669c 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/translations/et.json b/homeassistant/components/wled/translations/et.json
new file mode 100644
index 00000000000..794bd87e42f
--- /dev/null
+++ b/homeassistant/components/wled/translations/et.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "See WLED seade on juba konfigureeritud.",
+ "connection_error": "WLED-seadmega \u00fchenduse loomine nurjus."
+ },
+ "error": {
+ "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 soovite lisada WLED {nimi} Home Assistanti?",
+ "title": "Leitud WLED seade"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json
index 794f68256a7..ebbc0e52cde 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/pl.json b/homeassistant/components/wled/translations/pl.json
index 6f68055d385..608fbe65ef2 100644
--- a/homeassistant/components/wled/translations/pl.json
+++ b/homeassistant/components/wled/translations/pl.json
@@ -1,11 +1,11 @@
{
"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."
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"flow_title": "WLED: {name}",
"step": {
diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index 9a272c502a0..611fa7da315 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 c188c090369..633318f2f62 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 97f48e27988..1cae006824b 100644
--- a/homeassistant/components/wolflink/sensor.py
+++ b/homeassistant/components/wolflink/sensor.py
@@ -11,13 +11,6 @@ from wolf_smartset.models import (
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.const import (
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from .const import COORDINATOR, DEVICE_ID, DOMAIN, PARAMETERS, STATES
+
_LOGGER = logging.getLogger(__name__)
@@ -63,6 +58,7 @@ class WolfLinkSensor(CoordinatorEntity):
super().__init__(coordinator)
self.wolf_object = wolf_object
self.device_id = device_id
+ self._state = None
@property
def name(self):
@@ -71,8 +67,10 @@ class WolfLinkSensor(CoordinatorEntity):
@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 @@ class WolfLinkState(WolfLinkSensor):
@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 2ce7df6fae5..75c8199a117 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 00000000000..cb7e571d1e6
--- /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
new file mode 100644
index 00000000000..6e3348c3647
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/fr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "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"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json
new file mode 100644
index 00000000000..3b2d79a34a7
--- /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 00000000000..4d00f0bfc74
--- /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 483c73aac3d..d6d42eafb8a 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 00000000000..ef60c1c1ae1
--- /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 ea60e233907..bd505e845ae 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 00000000000..57cd8435f35
--- /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 fcd93f0b01b..75aa91bcf9c 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 c2be9263bcf..d9c90824742 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 2d9c4f5c9c1..e1bd79b7ea0 100644
--- a/homeassistant/components/wunderground/sensor.py
+++ b/homeassistant/components/wunderground/sensor.py
@@ -23,7 +23,9 @@ from homeassistant.const import (
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 @@ SENSOR_TYPES = {
"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 @@ SENSOR_TYPES = {
"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 @@ SENSOR_TYPES = {
"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/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py
index fecdfd91a75..d63be7824ed 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
@@ -299,7 +303,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
"Door Window Sensor",
xiaomi_hub,
data_key,
- "opening",
+ DEVICE_CLASS_OPENING,
config_entry,
)
@@ -349,7 +353,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
"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 fcfa3939c2c..1cc3b2d4633 100644
--- a/homeassistant/components/xiaomi_aqara/const.py
+++ b/homeassistant/components/xiaomi_aqara/const.py
@@ -36,10 +36,12 @@ BATTERY_MODELS = [
"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 1a00fc3afd2..4b6cd76985d 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 4a6c7ac14fd..5b1d3467d25 100644
--- a/homeassistant/components/xiaomi_aqara/sensor.py
+++ b/homeassistant/components/xiaomi_aqara/sensor.py
@@ -9,8 +9,10 @@ from homeassistant.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
TEMP_CELSIUS,
)
@@ -23,8 +25,8 @@ SENSOR_TYPES = {
"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/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json
index 534a6b3654e..22d3bbaccd3 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ca.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ca.json
@@ -7,7 +7,7 @@
},
"error": {
"discovery_error": "No s'ha pogut descobrir cap passarel\u00b7la Xiaomi Aqara, prova d'utilitzar la IP del dispositiu que executa Home Assistant com a interf\u00edcie",
- "invalid_host": "Adre\u00e7a IP no v\u00e0lida",
+ "invalid_host": "Adre\u00e7a IP no v\u00e0lida, consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
"invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida",
"invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida",
"invalid_mac": "Adre\u00e7a MAC no v\u00e0lida",
diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json
new file mode 100644
index 00000000000..75aa3d537e8
--- /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/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json
index a46dc756390..c5e03cc5c14 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 00000000000..3b2d79a34a7
--- /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 6b7f6d907ae..dfffec72272 100644
--- a/homeassistant/components/xiaomi_aqara/translations/it.json
+++ b/homeassistant/components/xiaomi_aqara/translations/it.json
@@ -7,7 +7,7 @@
},
"error": {
"discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, provare a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia",
- "invalid_host": "Indirizzo IP non valido",
+ "invalid_host": "Indirizzo IP non valido, vedere https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
"invalid_interface": "Interfaccia di rete non valida",
"invalid_key": "Chiave gateway non valida",
"invalid_mac": "Indirizzo Mac non valido",
diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json
index 90a22ace2b8..f222ac58bab 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 0119813e3e4..39f472299d7 100644
--- a/homeassistant/components/xiaomi_aqara/translations/no.json
+++ b/homeassistant/components/xiaomi_aqara/translations/no.json
@@ -13,8 +13,12 @@
"invalid_mac": "Ugyldig Mac-adresse",
"not_found_error": "Zeroconf oppdaget Gateway kunne ikke v\u00e6re plassert for \u00e5 f\u00e5 den n\u00f8dvendige informasjonen, kan du pr\u00f8ve \u00e5 bruke IP-adressen til enheten som kj\u00f8rer HomeAssistant som grensesnitt"
},
+ "flow_title": "",
"step": {
"select": {
+ "data": {
+ "select_ip": ""
+ },
"description": "Kj\u00f8r oppsettet igjen hvis du vil koble til tilleggsportaler",
"title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til"
},
@@ -32,7 +36,8 @@
"interface": "Nettverksgrensesnittet som skal brukes",
"mac": "Mac-adresse (valgfritt)"
},
- "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og mac-adressene er tomme, brukes automatisk oppdagelse"
+ "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og mac-adressene er tomme, brukes automatisk oppdagelse",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json
index a603566d569..fba37ad0249 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/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json
index 1983f8b28a5..a1340f587c9 100644
--- a/homeassistant/components/xiaomi_aqara/translations/pt.json
+++ b/homeassistant/components/xiaomi_aqara/translations/pt.json
@@ -4,6 +4,11 @@
"invalid_host": "Endere\u00e7o IP Inv\u00e1lido"
},
"step": {
+ "select": {
+ "data": {
+ "select_ip": ""
+ }
+ },
"settings": {
"data": {
"name": "Nome da Gateway"
diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py
index 15dc1bea8bd..6e25750ea50 100644
--- a/homeassistant/components/xiaomi_miio/sensor.py
+++ b/homeassistant/components/xiaomi_miio/sensor.py
@@ -21,6 +21,7 @@ from homeassistant.const import (
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ LIGHT_LUX,
PERCENTAGE,
PRESSURE_HPA,
TEMP_CELSIUS,
@@ -307,7 +308,7 @@ class XiaomiGatewayIlluminanceSensor(Entity):
@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/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json
index 6ec92566ade..d52715249b9 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/no.json b/homeassistant/components/xiaomi_miio/translations/no.json
index ff0ba48d98a..cf978d4a015 100644
--- a/homeassistant/components/xiaomi_miio/translations/no.json
+++ b/homeassistant/components/xiaomi_miio/translations/no.json
@@ -23,7 +23,8 @@
"data": {
"gateway": "Koble til en Xiaomi Gateway"
},
- "description": "Velg hvilken enhet du vil koble til."
+ "description": "Velg hvilken enhet du vil koble til.",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index 90f191a58c4..0c32eaf77ca 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/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json
new file mode 100644
index 00000000000..6930fca0a5b
--- /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/es.json b/homeassistant/components/yeelight/translations/es.json
index 08c2ca92dea..fdfbdfc5634 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 00000000000..3b2d79a34a7
--- /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/it.json b/homeassistant/components/yeelight/translations/it.json
index e3d965661b5..1c887037b8e 100644
--- a/homeassistant/components/yeelight/translations/it.json
+++ b/homeassistant/components/yeelight/translations/it.json
@@ -15,9 +15,10 @@
},
"user": {
"data": {
+ "host": "Host",
"ip_address": "Indirizzo IP"
},
- "description": "Se lasci vuoto l'indirizzo IP, verr\u00e0 utilizzato il rilevamento per trovare i dispositivi."
+ "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi."
}
}
},
diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json
new file mode 100644
index 00000000000..c04006e2c8f
--- /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 00000000000..255df06effe
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/lb.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Apparat"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Modell (Optionell)",
+ "use_music_mode": "Musek Modus aktiv\u00e9ieren"
+ }
+ }
+ }
+ },
+ "title": "Yeelight"
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/nb.json b/homeassistant/components/yeelight/translations/nb.json
new file mode 100644
index 00000000000..dd84563c39c
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/nb.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..f9f78ffb6f6
--- /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 4a2636457aa..325e3cbc7ae 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 00000000000..9fdd341e941
--- /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 0aa5dea0687..4852e874672 100644
--- a/homeassistant/components/zamg/sensor.py
+++ b/homeassistant/components/zamg/sensor.py
@@ -12,6 +12,7 @@ import requests
import voluptuous as vol
from homeassistant.const import (
+ AREA_SQUARE_METERS,
ATTR_ATTRIBUTION,
CONF_LATITUDE,
CONF_LONGITUDE,
@@ -20,6 +21,7 @@ from homeassistant.const import (
DEGREE,
LENGTH_METERS,
PERCENTAGE,
+ PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
__version__,
@@ -41,8 +43,8 @@ DEFAULT_NAME = "zamg"
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 @@ SENSOR_TYPES = {
"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/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 51da3638a9e..68300adbcfe 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 @@ CONFIG_SCHEMA = vol.Schema(
@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 @@ class HaZeroconf(Zeroconf):
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
+
+def _register_hass_zc_service(hass, zeroconf, uuid):
# Get instance UUID
- uuid = asyncio.run_coroutine_threadsafe(
- hass.helpers.instance_id.async_get(), hass.loop
- ).result()
-
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 = {}
- 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)
def service_update(zeroconf, service_type, name, state_change):
"""Service state changed."""
@@ -292,25 +307,8 @@ def setup(hass, config):
)
)
- 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 192afd87e65..a0bb855fc5d 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 bdfeb7815c5..a5b409c7116 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -23,6 +23,7 @@ from .core.const import (
ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_IEEE,
ATTR_LEVEL,
ATTR_MANUFACTURER,
ATTR_MEMBERS,
@@ -54,7 +55,12 @@ from .core.const import (
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 @@ DEVICE_INFO = "device_info"
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_WARNING_DEVICE_WARN = "warning_device_warn"
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 @@ ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type n
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
- {
- vol.Required("type"): "zha/devices/permit",
- 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 @@ async def websocket_permit_devices(hass, connection, msg):
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 @@ def async_load_api(hass):
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/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py
index ebc2cd5cd0f..0570070785f 100644
--- a/homeassistant/components/zha/core/channels/base.py
+++ b/homeassistant/components/zha/core/channels/base.py
@@ -57,7 +57,13 @@ def decorate_command(channel, command):
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 9138ea09782..a4bd2bf2d40 100644
--- a/homeassistant/components/zha/core/channels/smartenergy.py
+++ b/homeassistant/components/zha/core/channels/smartenergy.py
@@ -3,7 +3,13 @@ import logging
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 402c1505415..22f8f0f261d 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -35,6 +35,7 @@ ATTR_COMMAND_TYPE = "command_type"
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_MANUFACTURER_CODE = "manufacturer_code"
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 b8229793a48..68fb7393cd5 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -9,7 +9,7 @@ from typing import Any, Dict
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 @@ from .const import (
ATTR_DEVICE_IEEE,
ATTR_DEVICE_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_ENDPOINT_NAMES,
ATTR_ENDPOINTS,
ATTR_IEEE,
ATTR_LAST_SEEN,
@@ -41,6 +42,7 @@ from .const import (
ATTR_MANUFACTURER_CODE,
ATTR_MODEL,
ATTR_NAME,
+ ATTR_NEIGHBORS,
ATTR_NODE_DESCRIPTOR,
ATTR_NWK,
ATTR_POWER_SOURCE,
@@ -436,6 +438,39 @@ class ZHADevice(LogMixin):
}
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 @@ class ZHADevice(LogMixin):
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 7813c7133ad..0e967a7a123 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -6,15 +6,19 @@ https://home-assistant.io/integrations/zha/
"""
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 @@ def retryable_req(
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/manifest.json b/homeassistant/components/zha/manifest.json
index b03d4afd971..414a8721275 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.2",
+ "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.10.0",
- "zigpy==0.24.1",
+ "zigpy==0.26.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 6e2878f371b..a18d6bfa9dd 100644
--- a/homeassistant/components/zha/sensor.py
+++ b/homeassistant/components/zha/sensor.py
@@ -14,8 +14,10 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ PRESSURE_HPA,
STATE_UNKNOWN,
TEMP_CELSIUS,
)
@@ -234,7 +236,7 @@ class Illuminance(Sensor):
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_ILLUMINANCE
- _unit = "lx"
+ _unit = LIGHT_LUX
@staticmethod
def formatter(value):
@@ -266,7 +268,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 257d1026f7f..74793d6000f 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/translations/et.json b/homeassistant/components/zha/translations/et.json
new file mode 100644
index 00000000000..27a7113e3d2
--- /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/fr.json b/homeassistant/components/zha/translations/fr.json
index 1bfe4e5a3ac..1abfb3a9502 100644
--- a/homeassistant/components/zha/translations/fr.json
+++ b/homeassistant/components/zha/translations/fr.json
@@ -70,6 +70,13 @@
"device_shaken": "Appareil secou\u00e9",
"device_slid": "Appareil gliss\u00e9 \"{subtype}\"",
"device_tilted": "Dispositif inclin\u00e9",
+ "remote_button_alt_double_press": "Double-clic sur le bouton \" {subtype} \" (mode alternatif)",
+ "remote_button_alt_long_press": "Bouton \" {subtype} \" enfonc\u00e9 en continu (mode alternatif)",
+ "remote_button_alt_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long (mode alternatif)",
+ "remote_button_alt_quadruple_press": "Bouton \" {subtype} \" quadruple cliqu\u00e9 (mode alternatif)",
+ "remote_button_alt_quintuple_press": "Bouton \" {subtype} \" quintuple cliqu\u00e9 (mode alternatif)",
+ "remote_button_alt_short_press": "Bouton \" {subtype} \" appuy\u00e9 (mode alternatif)",
+ "remote_button_alt_short_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 (mode alternatif)",
"remote_button_alt_triple_press": "\"{subtype}\" bouton triple-cliqu\u00e9 (mode alternatif)",
"remote_button_double_press": "Double clic sur le bouton \" {subtype} \"",
"remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement",
diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json
index c5112235239..e97a4e27ccd 100644
--- a/homeassistant/components/zha/translations/no.json
+++ b/homeassistant/components/zha/translations/no.json
@@ -27,7 +27,8 @@
"data": {
"path": "Seriell enhetsbane"
},
- "description": "Velg seriell port for Zigbee radio"
+ "description": "Velg seriell port for Zigbee radio",
+ "title": ""
}
}
},
diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py
new file mode 100644
index 00000000000..d00cc560f22
--- /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 00000000000..c3e7f13d5e3
--- /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 00000000000..9d38c2cff39
--- /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 00000000000..06bd52f6bf5
--- /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 00000000000..e33465967e3
--- /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 00000000000..f4699838cde
--- /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 00000000000..d60bd068b89
--- /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 00000000000..df3931e6346
--- /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 00000000000..cd671e146ed
--- /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 00000000000..fbd9d1bd653
--- /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 00000000000..caf26a0130e
--- /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 00000000000..8c492c29f0b
--- /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 00000000000..f814476b9cd
--- /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 00000000000..0a9fc83cdea
--- /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 00000000000..65ae5095c39
--- /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 00000000000..c07b20de21b
--- /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 00000000000..dea02eb8ce7
--- /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 00000000000..7aecf4724a1
--- /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 00000000000..3a314918428
--- /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 00000000000..938a5b6cbe5
--- /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 aa921f376e7..8a319fb7c1d 100644
--- a/homeassistant/components/zone/translations/et.json
+++ b/homeassistant/components/zone/translations/et.json
@@ -4,13 +4,14 @@
"init": {
"data": {
"icon": "Ikoon",
- "latitude": "Laius",
- "longitude": "Pikkus",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
"name": "Nimi",
"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/zone/translations/no.json b/homeassistant/components/zone/translations/no.json
index 415c0a6afaa..9bf6e189369 100644
--- a/homeassistant/components/zone/translations/no.json
+++ b/homeassistant/components/zone/translations/no.json
@@ -10,7 +10,8 @@
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
"name": "Navn",
- "passive": "Passiv"
+ "passive": "Passiv",
+ "radius": ""
},
"title": "Definer sone parametere"
}
diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py
index 739864fdea8..73d6877ef2d 100644
--- a/homeassistant/components/zoneminder/binary_sensor.py
+++ b/homeassistant/components/zoneminder/binary_sensor.py
@@ -1,5 +1,8 @@
"""Support for ZoneMinder binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorEntity,
+)
from . import DOMAIN as ZONEMINDER_DOMAIN
@@ -35,7 +38,7 @@ class ZMAvailabilitySensor(BinarySensorEntity):
@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/translations/ca.json b/homeassistant/components/zoneminder/translations/ca.json
new file mode 100644
index 00000000000..cdbe838c1f1
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/ca.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.",
+ "connection_error": "No s'ha pogut connectar al servidor ZoneMinder."
+ },
+ "create_entry": {
+ "default": "S'ha afegit el servidor ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.",
+ "connection_error": "No s'ha pogut connectar al servidor ZoneMinder."
+ },
+ "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 SSL per a les connexions a ZoneMinder",
+ "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 00000000000..1362dcbd62d
--- /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 00000000000..8a63dab388f
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/el.json
@@ -0,0 +1,20 @@
+{
+ "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."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "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"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..3bf249c0786
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/en.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Username or password is incorrect.",
+ "connection_error": "Failed to connect to a ZoneMinder server."
+ },
+ "create_entry": {
+ "default": "ZoneMinder server added."
+ },
+ "error": {
+ "auth_fail": "Username or password is incorrect.",
+ "connection_error": "Failed to connect to a ZoneMinder server."
+ },
+ "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": "Use SSL for connections to ZoneMinder",
+ "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 00000000000..b7fa166864a
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/es.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.",
+ "connection_error": "No se pudo conectar con un servidor ZoneMinder."
+ },
+ "create_entry": {
+ "default": "Servidor ZoneMinder a\u00f1adido."
+ },
+ "error": {
+ "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.",
+ "connection_error": "No se pudo conectar con un servidor ZoneMinder."
+ },
+ "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 00000000000..1446846940a
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Kasutajanimi v\u00f5i salas\u00f5na on vale.",
+ "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus."
+ },
+ "create_entry": {
+ "default": "ZoneMinderi server on lisatud."
+ },
+ "error": {
+ "auth_fail": "Vale kasutajanimi v\u00f5i salas\u00f5na",
+ "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus."
+ },
+ "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 00000000000..0919811fa2b
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/fr.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "L'identifiant ou le mot de passe est incorrect.",
+ "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder."
+ },
+ "create_entry": {
+ "default": "Serveur Zoneminder ajout\u00e9."
+ },
+ "error": {
+ "auth_fail": "L'identifiant ou le mot de passe est incorrect.",
+ "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder."
+ },
+ "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 00000000000..b4f6b8da9ee
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/it.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nome utente o password non corretti.",
+ "connection_error": "Impossibile connettersi a un server ZoneMinder."
+ },
+ "create_entry": {
+ "default": "Server ZoneMinder aggiunto."
+ },
+ "error": {
+ "auth_fail": "Nome utente o password non corretti.",
+ "connection_error": "Impossibile connettersi a un server ZoneMinder."
+ },
+ "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": "Usa SSL per le connessioni a ZoneMinder",
+ "username": "Nome utente",
+ "verify_ssl": "Verifica del 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 00000000000..3625d6e402e
--- /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 00000000000..ad0669b1040
--- /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 00000000000..a9ad121c32e
--- /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 00000000000..3e5dc0867c0
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/no.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Brukernavn eller passord er feil.",
+ "connection_error": "Kunne ikke koble til en ZoneMinder-server."
+ },
+ "create_entry": {
+ "default": "ZoneMinder-serveren er lagt til."
+ },
+ "error": {
+ "auth_fail": "Brukernavn eller passord er feil.",
+ "connection_error": "Kunne ikke koble til en ZoneMinder-server."
+ },
+ "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": "Bruk SSL for tilkoblinger til ZoneMinder",
+ "username": "Brukernavn",
+ "verify_ssl": "Bekreft 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 00000000000..b8b737c37a3
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/pl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ 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 00000000000..e7ac29e58ab
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/ru.json
@@ -0,0 +1,30 @@
+{
+ "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.",
+ "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."
+ },
+ "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.",
+ "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."
+ },
+ "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\u043e\u0432\u0430\u0442\u044c SSL \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
+ "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 00000000000..37fd73d32f0
--- /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 00000000000..5d7c96c2ad2
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/zh-Hant.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4\u3002",
+ "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002"
+ },
+ "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",
+ "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002"
+ },
+ "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/TLS \u9023\u7dda\u81f3 ZoneMinder",
+ "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/translations/et.json b/homeassistant/components/zwave/translations/et.json
index e33b5e32827..07d5d386548 100644
--- a/homeassistant/components/zwave/translations/et.json
+++ b/homeassistant/components/zwave/translations/et.json
@@ -7,8 +7,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/config.py b/homeassistant/config.py
index 8f9dc7c3d62..3e9dd27458d 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -488,7 +488,6 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
CONF_UNIT_SYSTEM,
CONF_EXTERNAL_URL,
CONF_INTERNAL_URL,
- CONF_MEDIA_DIRS,
]
):
hac.config_source = SOURCE_YAML
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 347ca294d34..139e2066d17 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -39,6 +39,9 @@ SOURCE_IGNORE = "ignore"
# 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 3ed80859fa5..35b4b234597 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,13 +1,13 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 115
-PATCH_VERSION = "6"
+MINOR_VERSION = 116
+PATCH_VERSION = "0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)
# Truthy date string triggers showing related deprecation warning messages.
REQUIRED_NEXT_PYTHON_VER = (3, 8, 0)
-REQUIRED_NEXT_PYTHON_DATE = ""
+REQUIRED_NEXT_PYTHON_DATE = "December 7, 2020"
# Format for platform files
PLATFORM_FORMAT = "{platform}.{domain}"
@@ -381,10 +381,15 @@ ELECTRICAL_VOLT_AMPERE = f"{VOLT}{ELECTRICAL_CURRENT_AMPERE}"
# 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"
@@ -398,6 +403,7 @@ TIME_MONTHS = "m"
TIME_YEARS = "y"
# Length units
+LENGTH_MILLIMETERS: str = "mm"
LENGTH_CENTIMETERS: str = "cm"
LENGTH_METERS: str = "m"
LENGTH_KILOMETERS: str = "km"
@@ -423,6 +429,7 @@ PRESSURE_PSI: str = "psi"
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."
@@ -442,6 +449,9 @@ MASS_POUNDS: str = "lb"
# Conductivity units
CONDUCTIVITY: str = f"µS/{LENGTH_CENTIMETERS}"
+# Light units
+LIGHT_LUX: str = "lx"
+
# UV Index units
UV_INDEX: str = "UV index"
@@ -565,6 +575,7 @@ URL_API_TEMPLATE = "/api/template"
HTTP_OK = 200
HTTP_CREATED = 201
+HTTP_ACCEPTED = 202
HTTP_MOVED_PERMANENTLY = 301
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
@@ -574,6 +585,7 @@ HTTP_METHOD_NOT_ALLOWED = 405
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"
@@ -611,3 +623,7 @@ CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"]
# The ID of the Home Assistant Cast App
CAST_APP_ID_HOMEASSISTANT = "B12CE3CA"
+
+# The tracker error allow when converting
+# loop time to human readable time
+MAX_TIME_TRACKING_ERROR = 0.001
diff --git a/homeassistant/core.py b/homeassistant/core.py
index f230fef01eb..82fbe1be2b6 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -538,7 +538,7 @@ class Event:
event_type: str,
data: Optional[Dict[str, Any]] = None,
origin: EventOrigin = EventOrigin.local,
- time_fired: Optional[int] = None,
+ time_fired: Optional[datetime.datetime] = None,
context: Optional[Context] = None,
) -> None:
"""Initialize a new event."""
@@ -548,6 +548,11 @@ class Event:
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.
@@ -754,6 +759,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 +770,7 @@ class State:
"last_updated",
"context",
"domain",
+ "object_id",
]
def __init__(
@@ -797,12 +804,7 @@ class State:
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)
@property
def name(self) -> str:
@@ -907,7 +909,7 @@ class StateMachine:
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 +920,24 @@ class StateMachine:
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(
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 86d778db825..095c8c9afec 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -13,6 +13,7 @@ FLOWS = [
"agent_dvr",
"airly",
"airvisual",
+ "alarmdecoder",
"almond",
"ambiclimate",
"ambient_station",
@@ -30,6 +31,7 @@ FLOWS = [
"broadlink",
"brother",
"bsblan",
+ "canary",
"cast",
"cert_expiry",
"control4",
@@ -66,6 +68,7 @@ FLOWS = [
"geonetnz_volcano",
"gios",
"glances",
+ "goalzero",
"gogogate2",
"gpslogger",
"griddy",
@@ -125,6 +128,7 @@ FLOWS = [
"nut",
"nws",
"nzbget",
+ "omnilogic",
"onvif",
"opentherm_gw",
"openuv",
@@ -151,6 +155,7 @@ FLOWS = [
"roku",
"roomba",
"roon",
+ "rpi_power",
"samsungtv",
"sense",
"sentry",
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index ba49666ded3..1dfd797306f 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -69,6 +69,11 @@ ZEROCONF = {
"domain": "homekit_controller"
}
],
+ "_homekit._tcp.local.": [
+ {
+ "domain": "homekit"
+ }
+ ],
"_http._tcp.local.": [
{
"domain": "shelly",
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index f67b9a4b0ab..c982b58d8d9 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -297,7 +297,7 @@ def async_numeric_state_from_config(
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
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 282e63e6440..08d23a98da6 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -929,22 +929,44 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
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 1cf1fa4545c..e686dd2ae4b 100644
--- a/homeassistant/helpers/data_entry_flow.py
+++ b/homeassistant/helpers/data_entry_flow.py
@@ -8,7 +8,7 @@ import voluptuous as vol
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 @@ class FlowManagerIndexView(_BaseFlowManagerView):
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 @@ class FlowManagerResourceView(_BaseFlowManagerView):
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/entity_platform.py b/homeassistant/helpers/entity_platform.py
index da1a3635d72..39b09cef193 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -546,7 +546,7 @@ class EntityPlatform:
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 3033b51b605..b6d59bb500c 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -27,6 +27,7 @@ from homeassistant.const import (
EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED,
MATCH_ALL,
+ MAX_TIME_TRACKING_ERROR,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
)
@@ -40,6 +41,7 @@ from homeassistant.core import (
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
+from homeassistant.helpers.ratelimit import KeyedRateLimit
from homeassistant.helpers.sun import get_astral_event_next
from homeassistant.helpers.template import RenderInfo, Template, result_as_boolean
from homeassistant.helpers.typing import TemplateVarsType
@@ -47,30 +49,51 @@ from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
-MAX_TIME_TRACKING_ERROR = 0.001
-
TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks"
TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener"
TRACK_STATE_ADDED_DOMAIN_CALLBACKS = "track_state_added_domain_callbacks"
TRACK_STATE_ADDED_DOMAIN_LISTENER = "track_state_added_domain_listener"
+TRACK_STATE_REMOVED_DOMAIN_CALLBACKS = "track_state_removed_domain_callbacks"
+TRACK_STATE_REMOVED_DOMAIN_LISTENER = "track_state_removed_domain_listener"
+
TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks"
TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener"
+_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
@@ -235,10 +258,7 @@ def async_track_state_change_event(
EVENT_STATE_CHANGED, _async_state_change_dispatcher
)
- if isinstance(entity_ids, str):
- entity_ids = [entity_ids]
-
- entity_ids = [entity_id.lower() for entity_id in entity_ids]
+ entity_ids = _async_string_to_lower_list(entity_ids)
for entity_id in entity_ids:
entity_callbacks.setdefault(entity_id, []).append(action)
@@ -315,10 +335,7 @@ def async_track_entity_registry_updated_event(
EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_updated_dispatcher
)
- if isinstance(entity_ids, str):
- entity_ids = [entity_ids]
-
- entity_ids = [entity_id.lower() for entity_id in entity_ids]
+ entity_ids = _async_string_to_lower_list(entity_ids)
for entity_id in entity_ids:
entity_callbacks.setdefault(entity_id, []).append(action)
@@ -337,6 +354,26 @@ def async_track_entity_registry_updated_event(
return remove_listener
+@callback
+def _async_dispatch_domain_event(
+ hass: HomeAssistant, event: Event, callbacks: Dict[str, List]
+) -> None:
+ domain = split_entity_id(event.data["entity_id"])[0]
+
+ if domain not in callbacks and MATCH_ALL not in callbacks:
+ return
+
+ listeners = callbacks.get(domain, []) + callbacks.get(MATCH_ALL, [])
+
+ for action in listeners:
+ try:
+ hass.async_run_job(action, event)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Error while processing event %s for domain %s", event, domain
+ )
+
+
@bind_hass
def async_track_state_added_domain(
hass: HomeAssistant,
@@ -355,27 +392,13 @@ def async_track_state_added_domain(
if event.data.get("old_state") is not None:
return
- domain = split_entity_id(event.data["entity_id"])[0]
-
- if domain not in domain_callbacks:
- return
-
- for action in domain_callbacks[domain][:]:
- try:
- hass.async_run_job(action, event)
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception(
- "Error while processing state added for %s", domain
- )
+ _async_dispatch_domain_event(hass, event, domain_callbacks)
hass.data[TRACK_STATE_ADDED_DOMAIN_LISTENER] = hass.bus.async_listen(
EVENT_STATE_CHANGED, _async_state_change_dispatcher
)
- if isinstance(domains, str):
- domains = [domains]
-
- domains = [domains.lower() for domains in domains]
+ domains = _async_string_to_lower_list(domains)
for domain in domains:
domain_callbacks.setdefault(domain, []).append(action)
@@ -394,6 +417,209 @@ def async_track_state_added_domain(
return remove_listener
+@bind_hass
+def async_track_state_removed_domain(
+ hass: HomeAssistant,
+ domains: Union[str, Iterable[str]],
+ action: Callable[[Event], Any],
+) -> Callable[[], None]:
+ """Track state change events when an entity is removed from domains."""
+
+ domain_callbacks = hass.data.setdefault(TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {})
+
+ if TRACK_STATE_REMOVED_DOMAIN_LISTENER not in hass.data:
+
+ @callback
+ def _async_state_change_dispatcher(event: Event) -> None:
+ """Dispatch state changes by entity_id."""
+ if event.data.get("new_state") is not None:
+ return
+
+ _async_dispatch_domain_event(hass, event, domain_callbacks)
+
+ hass.data[TRACK_STATE_REMOVED_DOMAIN_LISTENER] = hass.bus.async_listen(
+ EVENT_STATE_CHANGED, _async_state_change_dispatcher
+ )
+
+ domains = _async_string_to_lower_list(domains)
+
+ for domain in domains:
+ domain_callbacks.setdefault(domain, []).append(action)
+
+ @callback
+ def remove_listener() -> None:
+ """Remove state change listener."""
+ _async_remove_indexed_listeners(
+ hass,
+ TRACK_STATE_REMOVED_DOMAIN_CALLBACKS,
+ TRACK_STATE_REMOVED_DOMAIN_LISTENER,
+ domains,
+ action,
+ )
+
+ return remove_listener
+
+
+@callback
+def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]:
+ if isinstance(instr, str):
+ return [instr.lower()]
+
+ 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(
@@ -499,17 +725,13 @@ class _TrackTemplateResultInfo:
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._info: Dict[Template, RenderInfo] = {}
- self._last_domains: Set = set()
- self._last_entities: Set = set()
- def async_setup(self) -> None:
+ self._rate_limit = KeyedRateLimit(hass)
+ self._info: Dict[Template, RenderInfo] = {}
+ self._track_state_changes: Optional[_TrackStateChangeFiltered] = 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
@@ -517,14 +739,17 @@ class _TrackTemplateResultInfo:
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,
@@ -534,195 +759,106 @@ class _TrackTemplateResultInfo:
@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)
-
- @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
-
- @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
-
- had_all_listener = self._all_listener is not None
- if had_all_listener:
- self._cancel_all_listener()
-
- entities, domains = _entities_domains_from_info(self._info.values())
- domains_changed = domains != self._last_domains
-
- if had_all_listener or domains_changed:
- domains_changed = True
- self._cancel_domains_listener()
- self._setup_domains_listener(domains)
-
- if had_all_listener or domains_changed or entities != self._last_entities:
- self._cancel_entities_listener()
- self._setup_entities_listener(domains, entities)
-
- self._last_domains = domains
- self._last_entities = entities
-
- @callback
- def _setup_entities_listener(self, domains: Set, entities: Set) -> None:
- if domains:
- entities = entities.copy()
- entities.update(self.hass.states.async_entity_ids(domains))
-
- # Entities has changed to none
- if not entities:
- return
-
- self._entities_listener = async_track_state_change_event(
- self.hass, entities, self._refresh
- )
-
- @callback
- def _setup_domains_listener(self, domains: Set) -> None:
- if not domains:
- return
-
- self._domains_listener = async_track_state_added_domain(
- self.hass, domains, self._refresh
- )
-
- @callback
- def _setup_all_listener(self) -> None:
- self._all_listener = self.hass.bus.async_listen(
- EVENT_STATE_CHANGED, self._refresh
- )
+ assert self._track_state_changes
+ return self._track_state_changes.listeners
@callback
def async_remove(self) -> None:
"""Cancel the listener."""
- self._cancel_all_listener()
- self._cancel_domains_listener()
- self._cancel_entities_listener()
+ assert self._track_state_changes
+ self._track_state_changes.async_remove()
+ self._rate_limit.async_remove()
@callback
def async_refresh(self) -> None:
"""Force recalculate the template."""
self._refresh(None)
- @callback
- def _refresh(self, event: Optional[Event]) -> None:
- entity_id = event and event.data.get(ATTR_ENTITY_ID)
- updates = []
- info_changed = False
+ def _render_template_if_ready(
+ self,
+ track_template_: TrackTemplate,
+ now: datetime,
+ event: Optional[Event],
+ ) -> Union[bool, TrackTemplateResult]:
+ """Re-render the template if conditions match.
- 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)
+ Returns False if the template was not be re-rendered
+
+ Returns True if the template re-rendered and did not
+ change.
+
+ Returns TrackTemplateResult if the template re-render
+ generates a new result.
+ """
+ template = track_template_.template
+
+ if event:
+ info = self._info[template]
+
+ if not self._rate_limit.async_has_timer(
+ template
+ ) and not _event_triggers_rerender(event, info):
+ return False
+
+ if self._rate_limit.async_schedule_action(
+ template,
+ _rate_limit_for_event(event, info, track_template_),
+ now,
+ self._refresh,
+ event,
):
- continue
+ return False
_LOGGER.debug(
- "Template update %s triggered by event: %s", template.template, event
+ "Template update %s triggered by event: %s",
+ template.template,
+ event,
)
- self._info[template] = template.async_render_to_info(
- track_template_.variables
- )
+ self._rate_limit.async_triggered(template, now)
+ self._info[template] = template.async_render_to_info(track_template_.variables)
+
+ 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:
+ return True
+
+ if isinstance(result, TemplateError) and isinstance(last_result, TemplateError):
+ return True
+
+ return TrackTemplateResult(template, last_result, result)
+
+ @callback
+ def _refresh(self, event: Optional[Event]) -> None:
+ updates = []
+ info_changed = False
+ now = dt_util.utcnow()
+
+ for track_template_ in self._track_templates:
+ update = self._render_template_if_ready(track_template_, now, event)
+ if not update:
+ continue
+
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()
+ 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,
)
- self._last_info = self._info.copy()
if not updates:
return
@@ -758,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.
@@ -779,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
-------
@@ -789,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
@@ -1176,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()
@@ -1186,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 8bdfc286c1a..9cff5058a00 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 00000000000..422ebdf2eee
--- /dev/null
+++ b/homeassistant/helpers/ratelimit.py
@@ -0,0 +1,97 @@
+"""Ratelimit helper."""
+import asyncio
+from datetime import datetime, timedelta
+import logging
+from typing import Any, Callable, Dict, Hashable, Optional
+
+from homeassistant.const import MAX_TIME_TRACKING_ERROR
+from homeassistant.core import HomeAssistant, callback
+import homeassistant.util.dt as dt_util
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class KeyedRateLimit:
+ """Class to track rate limits."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ ):
+ """Initialize ratelimit tracker."""
+ self.hass = hass
+ self._last_triggered: Dict[Hashable, datetime] = {}
+ self._rate_limit_timers: Dict[Hashable, asyncio.TimerHandle] = {}
+
+ @callback
+ def async_has_timer(self, key: Hashable) -> bool:
+ """Check if a rate limit timer is running."""
+ return key in self._rate_limit_timers
+
+ @callback
+ def async_triggered(self, key: Hashable, now: Optional[datetime] = None) -> None:
+ """Call when the action we are tracking was triggered."""
+ self.async_cancel_timer(key)
+ self._last_triggered[key] = now or dt_util.utcnow()
+
+ @callback
+ def async_cancel_timer(self, key: Hashable) -> None:
+ """Cancel a rate limit time that will call the action."""
+ if not self.async_has_timer(key):
+ return
+
+ self._rate_limit_timers.pop(key).cancel()
+
+ @callback
+ def async_remove(self) -> None:
+ """Remove all timers."""
+ for timer in self._rate_limit_timers.values():
+ timer.cancel()
+ self._rate_limit_timers.clear()
+
+ @callback
+ def async_schedule_action(
+ self,
+ key: Hashable,
+ rate_limit: Optional[timedelta],
+ now: datetime,
+ action: Callable,
+ *args: Any,
+ ) -> Optional[datetime]:
+ """Check rate limits and schedule an action if we hit the limit.
+
+ If the rate limit is hit:
+ Schedules the action for when the rate limit expires
+ if there are no pending timers. The action must
+ be called in async.
+
+ Returns the time the rate limit will expire
+
+ If the rate limit is not hit:
+
+ Return None
+ """
+ if rate_limit is None or key not in self._last_triggered:
+ return None
+
+ next_call_time = self._last_triggered[key] + rate_limit
+
+ if next_call_time <= now:
+ self.async_cancel_timer(key)
+ return None
+
+ _LOGGER.debug(
+ "Reached rate limit of %s for %s and deferred action until %s",
+ rate_limit,
+ key,
+ next_call_time,
+ )
+
+ if key not in self._rate_limit_timers:
+ self._rate_limit_timers[key] = self.hass.loop.call_later(
+ (next_call_time - now).total_seconds() + MAX_TIME_TRACKING_ERROR,
+ action,
+ *args,
+ )
+
+ return next_call_time
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 717e9c3980c..4d958fe431f 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 @@ class Script:
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 ad5a36467cf..20f7aa2d2d7 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -13,6 +13,7 @@ from typing import (
Optional,
Set,
Tuple,
+ Union,
)
import voluptuous as vol
@@ -43,10 +44,9 @@ from homeassistant.util.yaml.loader import JSON_TYPE
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 @@ def async_register_admin_service(
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 917581fac07..9c849bee22e 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -1,14 +1,16 @@
"""Template helper methods for rendering strings with Home Assistant data."""
+import asyncio
import base64
import collections.abc
-from datetime import datetime
+from datetime import datetime, timedelta
from functools import wraps
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.helpers.typing import HomeAssistantType, TemplateVarsType
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 @@ _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunctio
_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 @@ class RenderInfo:
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 @@ class RenderInfo:
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.domains:
- self.filter_lifecycle = self.filter
+ 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 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 @@ class Template:
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 @@ class Template:
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 @@ class AllStates:
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 @@ class AllStates:
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 @@ class DomainStates:
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 @@ class DomainStates:
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 _access_state(self):
- state = object.__getattribute__(self, "_state")
- hass = object.__getattribute__(self, "_hass")
- _collect_state(hass, state.entity_id)
- return state
+ 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
+
+ @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
@@ -1102,6 +1263,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["utcnow"] = dt_util.utcnow
self.globals["as_timestamp"] = forgiving_as_timestamp
self.globals["relative_time"] = relative_time
+ self.globals["timedelta"] = timedelta
self.globals["strptime"] = strptime
self.globals["urlencode"] = urlencode
if hass is None:
@@ -1136,12 +1298,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
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/package_constraints.txt b/homeassistant/package_constraints.txt
index d88ede1a1d9..cdda546a4e1 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==20200918.2
+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
diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py
index e5654e6f8c6..bf61c67172a 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 fix_threading_exception_logging() -> 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/requirements.txt b/requirements.txt
index baa48241a06..91f09a54390 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 4d9f14ee88c..6aa3e39b844 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2,7 +2,7 @@
-r requirements.txt
# homeassistant.components.nuimo_controller
---only-binary=all nuimo==0.1.0
+# --only-binary=all nuimo==0.1.0
# homeassistant.components.dht
# Adafruit-DHT==1.4.0
@@ -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
@@ -221,7 +221,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
-aioshelly==0.3.3
+aioshelly==0.3.4
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -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
@@ -339,7 +339,7 @@ beautifulsoup4==4.9.1
# beewi_smartclim==0.0.7
# homeassistant.components.zha
-bellows==0.20.2
+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
@@ -484,7 +484,7 @@ deluge-client==1.7.1
denonavr==0.9.4
# 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
@@ -538,7 +538,7 @@ elgato==0.2.0
eliqonline==1.2.2
# homeassistant.components.elkm1
-elkm1-lib==0.7.19
+elkm1-lib==0.8.0
# homeassistant.components.mobile_app
emoji==0.5.4
@@ -668,6 +668,9 @@ 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.3
@@ -722,6 +725,9 @@ hangups==0.4.11
# homeassistant.components.cloud
hass-nabucasa==0.37.0
+# homeassistant.components.splunk
+hass_splunk==0.1.1
+
# homeassistant.components.jewish_calendar
hdate==0.9.5
@@ -747,7 +753,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20200918.2
+home-assistant-frontend==20201001.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -771,6 +777,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
@@ -995,7 +1004,7 @@ numato-gpio==0.8.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.19.1
+numpy==1.19.2
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1006,6 +1015,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 +1059,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 +1110,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 +1186,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 +1197,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
@@ -1202,7 +1217,7 @@ pyRFXtrx==0.25
# pySwitchmate==0.4.6
# homeassistant.components.tibber
-pyTibber==0.14.0
+pyTibber==0.15.3
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1223,7 +1238,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 +1247,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
@@ -1467,7 +1482,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 +1536,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 +1580,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
@@ -1638,7 +1653,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
@@ -1761,7 +1776,7 @@ python-sochain-api==0.0.2
python-songpal==0.12
# homeassistant.components.synology_dsm
-python-synology==0.8.2
+python-synology==0.9.0
# homeassistant.components.tado
python-tado==0.8.1
@@ -1776,7 +1791,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
@@ -1846,7 +1861,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 +1938,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
@@ -1942,7 +1960,7 @@ saltbox==0.1.3
samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv
-samsungtvws[websocket]==1.4.0
+samsungtvws==1.4.0
# homeassistant.components.satel_integra
satel_integra==0.3.4
@@ -1964,7 +1982,7 @@ sense-hat==2.2.0
sense_energy==0.8.0
# homeassistant.components.sentry
-sentry-sdk==0.17.3
+sentry-sdk==0.18.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -1982,7 +2000,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 +2035,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 +2047,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 +2056,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 +2071,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
@@ -2247,7 +2265,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 +2277,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 +2299,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 +2308,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
@@ -2311,10 +2329,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.24.1
+zigpy==0.26.0
# homeassistant.components.zoneminder
zm-py==0.4.0
diff --git a/requirements_test.txt b/requirements_test.txt
index e36837edf63..0fd681b62b2 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 de0240710f3..deff4c9aafc 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -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
@@ -50,6 +50,9 @@ 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
@@ -131,7 +134,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
-aioshelly==0.3.3
+aioshelly==0.3.4
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -155,7 +158,7 @@ androidtv[async]==0.0.50
apns2==0.3.0
# homeassistant.components.apprise
-apprise==0.8.8
+apprise==0.8.9
# homeassistant.components.aprs
aprslib==0.6.46
@@ -183,7 +186,7 @@ azure-eventhub==5.1.0
base36==0.1.1
# homeassistant.components.zha
-bellows==0.20.2
+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
@@ -251,7 +254,7 @@ defusedxml==0.6.0
denonavr==0.9.4
# 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
@@ -275,7 +278,7 @@ eebrightbox==0.0.4
elgato==0.2.0
# homeassistant.components.elkm1
-elkm1-lib==0.7.19
+elkm1-lib==0.8.0
# homeassistant.components.mobile_app
emoji==0.5.4
@@ -333,6 +336,9 @@ 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.3
@@ -370,7 +376,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20200918.2
+home-assistant-frontend==20201001.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -388,6 +394,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
@@ -472,11 +481,14 @@ numato-gpio==0.8.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.19.1
+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 +500,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 +527,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 +576,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
@@ -583,7 +595,7 @@ pyMetno==0.8.1
pyRFXtrx==0.25
# homeassistant.components.tibber
-pyTibber==0.14.0
+pyTibber==0.15.3
# homeassistant.components.nextbus
py_nextbusnext==0.1.4
@@ -592,7 +604,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 +613,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
@@ -710,7 +722,7 @@ pylutron-caseta==0.6.1
pymailgunner==1.4
# homeassistant.components.firmata
-pymata-express==1.13
+pymata-express==1.19
# homeassistant.components.melcloud
pymelcloud==2.5.2
@@ -734,7 +746,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
@@ -788,7 +800,7 @@ 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
@@ -830,7 +842,7 @@ python-openzwave-mqtt==1.0.5
python-songpal==0.12
# homeassistant.components.synology_dsm
-python-synology==0.8.2
+python-synology==0.9.0
# homeassistant.components.tado
python-tado==0.8.1
@@ -839,7 +851,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
@@ -898,6 +910,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
@@ -905,14 +920,14 @@ rxv==0.6.0
samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv
-samsungtvws[websocket]==1.4.0
+samsungtvws==1.4.0
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense_energy==0.8.0
# homeassistant.components.sentry
-sentry-sdk==0.17.3
+sentry-sdk==0.18.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -921,7 +936,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==9.3.0
+simplisafe-python==9.4.1
# homeassistant.components.sleepiq
sleepyq==0.7
@@ -942,7 +957,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 +969,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
@@ -1040,7 +1055,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,7 +1071,7 @@ 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
@@ -1071,7 +1086,7 @@ 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.24.1
+zigpy==0.26.0
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 2daad6e33f0..d464c648e75 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -5,7 +5,7 @@ black==20.8b1
codespell==1.17.1
flake8-docstrings==1.5.0
flake8==3.8.3
-isort==5.5.1
+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 5e3a2d8b1f3..c3e489c1ebb 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -23,11 +23,13 @@ COMMENT_REQUIREMENTS = (
"bme680",
"credstash",
"decora",
+ "decora_wifi",
"env_canada",
"envirophat",
"evdev",
"face_recognition",
"i2csense",
+ "nuimo",
"opencv-python-headless",
"py_noaa",
"pybluez",
diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py
index 26af118d11e..e3e4fbf38c6 100644
--- a/script/hassfest/__main__.py
+++ b/script/hassfest/__main__.py
@@ -114,7 +114,7 @@ def main():
try:
start = monotonic()
print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True)
- if plugin is requirements:
+ if plugin is requirements and not config.specific_integrations:
print()
plugin.validate(integrations, config)
print(" done in {:.2f}s".format(monotonic() - start))
diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py
index ab43cd62bd5..b51cbff7185 100644
--- a/script/hassfest/requirements.py
+++ b/script/hassfest/requirements.py
@@ -1,5 +1,8 @@
"""Validate requirements."""
+from collections import deque
+import json
import operator
+import os
import re
import subprocess
import sys
@@ -27,6 +30,20 @@ SUPPORTED_PYTHON_VERSIONS = [
".".join(map(str, version_tuple)) for version_tuple in SUPPORTED_PYTHON_TUPLES
]
STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS}
+PIPDEPTREE_CACHE = None
+
+IGNORE_VIOLATIONS = {
+ # Still has standard library requirements.
+ "acmeda",
+ "blink",
+ "ezviz",
+ "hdmi_cec",
+ "juicenet",
+ "lupusec",
+ "rainbird",
+ "slide",
+ "suez_water",
+}
def normalize_package_name(requirement: str) -> str:
@@ -43,8 +60,13 @@ def normalize_package_name(requirement: str) -> str:
def validate(integrations: Dict[str, Integration], config: Config):
"""Handle requirements for integrations."""
+ ensure_cache()
+
# check for incompatible requirements
- for integration in tqdm(integrations.values()):
+
+ disable_tqdm = config.specific_integrations or os.environ.get("CI", False)
+
+ for integration in tqdm(integrations.values(), disable=disable_tqdm):
if not integration.manifest:
continue
@@ -53,6 +75,10 @@ def validate(integrations: Dict[str, Integration], config: Config):
def validate_requirements(integration: Integration):
"""Validate requirements."""
+ # Some integrations have not been fixed yet so are allowed to have violations.
+ if integration.domain in IGNORE_VIOLATIONS:
+ return
+
integration_requirements = set()
integration_packages = set()
for req in integration.requirements:
@@ -92,39 +118,68 @@ def validate_requirements(integration: Integration):
)
+def ensure_cache():
+ """Ensure we have a cache of pipdeptree.
+
+ {
+ "flake8-docstring": {
+ "key": "flake8-docstrings",
+ "package_name": "flake8-docstrings",
+ "installed_version": "1.5.0"
+ "dependencies": {"flake8"}
+ }
+ }
+ """
+ global PIPDEPTREE_CACHE
+
+ if PIPDEPTREE_CACHE is not None:
+ return
+
+ cache = {}
+
+ for item in json.loads(
+ subprocess.run(
+ ["pipdeptree", "-w", "silence", "--json"],
+ check=True,
+ capture_output=True,
+ text=True,
+ ).stdout
+ ):
+ cache[item["package"]["key"]] = {
+ **item["package"],
+ "dependencies": {dep["key"] for dep in item["dependencies"]},
+ }
+
+ PIPDEPTREE_CACHE = cache
+
+
def get_requirements(integration: Integration, packages: Set[str]) -> Set[str]:
"""Return all (recursively) requirements for an integration."""
+ ensure_cache()
+
all_requirements = set()
- for package in packages:
- try:
- result = subprocess.run(
- ["pipdeptree", "-w", "silence", "--packages", package],
- check=True,
- capture_output=True,
- text=True,
- )
- except subprocess.SubprocessError:
- integration.add_error(
- "requirements", f"Failed to resolve requirements for {package}"
- )
+ to_check = deque(packages)
+
+ while to_check:
+ package = to_check.popleft()
+
+ if package in all_requirements:
continue
- # parse output to get a set of package names
- output = result.stdout
- lines = output.split("\n")
- parent = lines[0].split("==")[0] # the first line is the parent package
- if parent:
- all_requirements.add(parent)
+ all_requirements.add(package)
- for line in lines[1:]: # skip the first line which we already processed
- line = line.strip()
- line = line.lstrip("- ")
- package = line.split("[")[0]
- package = package.strip()
- if not package:
- continue
- all_requirements.add(package)
+ item = PIPDEPTREE_CACHE.get(package)
+
+ if item is None:
+ # Only warn if direct dependencies could not be resolved
+ if package in packages:
+ integration.add_error(
+ "requirements", f"Failed to resolve requirements for {package}"
+ )
+ continue
+
+ to_check.extend(item["dependencies"])
return all_requirements
@@ -134,15 +189,11 @@ def install_requirements(integration: Integration, requirements: Set[str]) -> bo
Return True if successful.
"""
+ global PIPDEPTREE_CACHE
+
+ ensure_cache()
+
for req in requirements:
- try:
- is_installed = pkg_util.is_installed(req)
- except ValueError:
- is_installed = False
-
- if is_installed:
- continue
-
match = PIP_REGEX.search(req)
if not match:
@@ -155,17 +206,39 @@ def install_requirements(integration: Integration, requirements: Set[str]) -> bo
install_args = match.group(1)
requirement_arg = match.group(2)
+ is_installed = False
+
+ normalized = normalize_package_name(requirement_arg)
+
+ if normalized and "==" in requirement_arg:
+ ver = requirement_arg.split("==")[-1]
+ item = PIPDEPTREE_CACHE.get(normalized)
+ is_installed = item and item["installed_version"] == ver
+
+ if not is_installed:
+ try:
+ is_installed = pkg_util.is_installed(req)
+ except ValueError:
+ is_installed = False
+
+ if is_installed:
+ continue
+
args = [sys.executable, "-m", "pip", "install", "--quiet"]
if install_args:
args.append(install_args)
args.append(requirement_arg)
try:
- subprocess.run(args, check=True)
+ result = subprocess.run(args, check=True, capture_output=True, text=True)
except subprocess.SubprocessError:
integration.add_error(
"requirements",
f"Requirement {req} failed to install",
)
+ else:
+ # Clear the pipdeptree cache if something got installed
+ if "Successfully installed" in result.stdout:
+ PIPDEPTREE_CACHE = None
if integration.errors:
return False
diff --git a/setup.py b/setup.py
index 0bbdf9f05a8..022f5547655 100755
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,7 @@ REQUIRES = [
"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/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py
index b94d17066c8..4ce34dfebe9 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -6,7 +6,6 @@ from homeassistant.components.accuweather.const import (
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 @@ from homeassistant.const import (
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/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
index dae9e0da79d..e773768ebe6 100644
--- a/tests/components/adguard/test_config_flow.py
+++ b/tests/components/adguard/test_config_flow.py
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
from tests.async_mock import patch
@@ -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(
diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py
index 2cda0b8aa73..ec35b521a17 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 403dd9f4bee..064034a4a69 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 import data_entry_flow
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/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py
index 8912b0287d7..2d0acf8fe7b 100644
--- a/tests/components/airvisual/test_config_flow.py
+++ b/tests/components/airvisual/test_config_flow.py
@@ -31,7 +31,6 @@ async def test_duplicate_error(hass):
CONF_LATITUDE: 51.528308,
CONF_LONGITUDE: -0.3817765,
}
- node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"}
MockConfigEntry(
domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf
@@ -44,6 +43,8 @@ async def test_duplicate_error(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
+ node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"}
+
MockConfigEntry(
domain=DOMAIN, unique_id="192.168.1.100", data=node_pro_conf
).add_to_hass(hass)
@@ -68,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(
@@ -78,24 +79,6 @@ async def test_invalid_identifier(hass):
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
-async def test_node_pro_error(hass):
- """Test that an invalid Node/Pro ID shows an error."""
- node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
-
- with patch(
- "pyairvisual.node.Node.from_samba",
- side_effect=NodeProError,
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"}
- )
- result = await hass.config_entries.flow.async_configure(
- 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"}
-
-
async def test_migration(hass):
"""Test migrating from version 1 to the current version."""
conf = {
@@ -113,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})
@@ -142,6 +125,24 @@ async def test_migration(hass):
}
+async def test_node_pro_error(hass):
+ """Test that an invalid Node/Pro ID shows an error."""
+ node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
+
+ with patch(
+ "pyairvisual.node.NodeSamba.async_connect",
+ side_effect=NodeProError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ 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"}
+
+
async def test_options_flow(hass):
"""Test config flow options."""
geography_conf = {
@@ -184,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
)
@@ -198,13 +199,42 @@ async def test_step_geography(hass):
}
+async def test_step_import(hass):
+ """Test the import step for both types of configuration."""
+ geography_conf = {
+ CONF_API_KEY: "abcde12345",
+ CONF_LATITUDE: 51.528308,
+ CONF_LONGITUDE: -0.3817765,
+ }
+
+ with patch(
+ "homeassistant.components.airvisual.async_setup_entry", return_value=True
+ ), patch("pyairvisual.air_quality.AirQuality.nearest_city"):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "Cloud API (51.528308, -0.3817765)"
+ assert result["data"] == {
+ CONF_API_KEY: "abcde12345",
+ CONF_LATITUDE: 51.528308,
+ CONF_LONGITUDE: -0.3817765,
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
+ }
+
+
async def test_step_node_pro(hass):
"""Test the Node/Pro step."""
conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
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"}
)
@@ -220,29 +250,37 @@ async def test_step_node_pro(hass):
}
-async def test_step_import(hass):
- """Test the import step for both types of configuration."""
+async def test_step_reauth(hass):
+ """Test that the reauth step works."""
geography_conf = {
CONF_API_KEY: "abcde12345",
CONF_LATITUDE: 51.528308,
CONF_LONGITUDE: -0.3817765,
}
+ MockConfigEntry(
+ domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=geography_conf
+ )
+ assert result["step_id"] == "reauth_confirm"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
with patch(
"homeassistant.components.airvisual.async_setup_entry", return_value=True
- ), patch("pyairvisual.api.API.nearest_city"):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
+ ), patch("pyairvisual.air_quality.AirQuality"):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_API_KEY: "defgh67890"}
)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "reauth_successful"
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == "Cloud API (51.528308, -0.3817765)"
- assert result["data"] == {
- CONF_API_KEY: "abcde12345",
- CONF_LATITUDE: 51.528308,
- CONF_LONGITUDE: -0.3817765,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
- }
+ assert len(hass.config_entries.async_entries()) == 1
async def test_step_user(hass):
diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py
new file mode 100644
index 00000000000..64f4a604ff3
--- /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 8937a7938ac..c838bf5b3a3 100644
--- a/tests/components/alexa/test_intent.py
+++ b/tests/components/alexa/test_intent.py
@@ -6,6 +6,7 @@ import pytest
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 46db47dc2c9..c46a61aef41 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/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py
index 456a9313091..c497a9daa48 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/atag/__init__.py b/tests/components/atag/__init__.py
index 3f2b6468491..52d53ee9948 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 63cac2a0e9c..7609d6c3e54 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 0fca4b37c46..9f7ae9cb4ed 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 c471dfca2a9..93b64ebbd3f 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 2f20347acae..1c23976a6f9 100644
--- a/tests/components/august/test_config_flow.py
+++ b/tests/components/august/test_config_flow.py
@@ -17,6 +17,7 @@ from homeassistant.components.august.exceptions import (
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 ec035b9ec38..c1aa0723baa 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 bcc05e51c71..f954ff83c25 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 @@ from homeassistant.components.august.const import (
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.async_mock import patch
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 7e69b59da07..51e00b9d09f 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/automation/common.py b/tests/components/automation/common.py
deleted file mode 100644
index 26521f76d31..00000000000
--- 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 9c38574945d..1cdcfc11dfb 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -10,12 +10,16 @@ from homeassistant.components.automation import (
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 @@ import homeassistant.util.dt as dt_util
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 @@ async def test_automation_stops(hass, calls, service):
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 4d48959632d..3b013fad29c 100644
--- a/tests/components/awair/test_sensor.py
+++ b/tests/components/awair/test_sensor.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
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/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py
index a7660b03da5..bc0eba069a8 100644
--- a/tests/components/broadlink/test_config_flow.py
+++ b/tests/components/broadlink/test_config_flow.py
@@ -249,11 +249,11 @@ 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}
@@ -403,12 +403,12 @@ 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}
diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py
index 5b9d0e77508..d267243aeb9 100644
--- a/tests/components/broadlink/test_device.py
+++ b/tests/components/broadlink/test_device.py
@@ -20,16 +20,13 @@ from tests.common import mock_device_registry, mock_registry
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
@@ -183,15 +168,13 @@ async def test_device_setup_update_authentication_error(hass):
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
@@ -210,15 +193,13 @@ async def test_device_setup_update_broadlink_exception(hass):
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/bsblan/__init__.py b/tests/components/bsblan/__init__.py
index 45b0f16c0a1..511b566ce41 100644
--- a/tests/components/bsblan/__init__.py
+++ b/tests/components/bsblan/__init__.py
@@ -5,7 +5,7 @@ from homeassistant.components.bsblan.const import (
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 c24fbc34f6a..4c24e10a237 100644
--- a/tests/components/bsblan/test_config_flow.py
+++ b/tests/components/bsblan/test_config_flow.py
@@ -5,7 +5,7 @@ from homeassistant import data_entry_flow
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
@@ -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 c3db5067b1f..966adc97b67 100644
--- a/tests/components/camera/test_init.py
+++ b/tests/components/camera/test_init.py
@@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START
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 cc85edd806a..9d0e488d516 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 00000000000..0127865f6a1
--- /dev/null
+++ b/tests/components/canary/conftest.py
@@ -0,0 +1,58 @@
+"""Define fixtures available for all tests."""
+from canary.api import Api
+from pytest import fixture
+
+from tests.async_mock import MagicMock, patch
+
+
+def mock_canary_update(self, **kwargs):
+ """Get the latest data from py-canary."""
+ self._update(**kwargs)
+
+
+@fixture
+def canary(hass):
+ """Mock the CanaryApi for easier testing."""
+ with patch.object(Api, "login", return_value=True), patch(
+ "homeassistant.components.canary.CanaryData.update", mock_canary_update
+ ), patch("homeassistant.components.canary.Api") as mock_canary:
+ 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 00000000000..f1b8fc3396e
--- /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, False)
+
+ # 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, False)
+
+ # 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, False)
+
+ # 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 00000000000..36c6990a663
--- /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 0cfbfd56de6..f548a007505 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 requests import ConnectTimeout
-from homeassistant import setup
-import homeassistant.components.canary as canary
+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 tests.async_mock import MagicMock, PropertyMock, patch
-from tests.common import get_test_home_assistant
+from . import YAML_CONFIG, init_integration
+
+from tests.async_mock import patch
-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
+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
-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
+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"
-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
+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)
-class TestCanary(unittest.TestCase):
- """Tests the Canary component."""
+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()
- 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)
+ 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 f328fb5a976..13b82c9a996 100644
--- a/tests/components/canary/test_sensor.py
+++ b/tests/components/canary/test_sensor.py
@@ -1,203 +1,205 @@
"""The tests for the Canary sensor platform."""
-import copy
-import unittest
-
-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 homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ DEVICE_CLASS_TEMPERATURE,
+ PERCENTAGE,
+ TEMP_CELSIUS,
+)
+from homeassistant.setup import async_setup_component
-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
+from . import mock_device, mock_location, mock_reading
-VALID_CONFIG = {"canary": {"username": "foo@bar.org", "password": "bar"}}
+from tests.async_mock import patch
+from tests.common import mock_device_registry, mock_registry
-class TestCanarySensorSetup(unittest.TestCase):
- """Test the Canary platform."""
+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", {})
- DEVICES = []
+ registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
- def add_entities(self, devices, action):
- """Mock add devices."""
- for device in devices:
- self.DEVICES.append(device)
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
- 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)
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
- 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")
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.59"),
+ ]
- 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]),
- ]
+ 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()
- canary.setup_platform(self.hass, self.config, self.add_entities, None)
+ 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",
+ ),
+ }
- assert len(self.DEVICES) == 6
+ 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]
- def test_temperature_sensor(self):
- """Test temperature sensor with fahrenheit."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home", False)
+ 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]
- data = Mock()
- data.get_reading.return_value = 21.1234
+ 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"
- 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"
+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", {})
- 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)
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro")
- data = Mock()
- data.get_reading.return_value = None
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
- sensor = CanarySensor(data, SENSOR_TYPES[0], location, device)
- sensor.update()
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.59"),
+ ]
- assert sensor.state is None
+ 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()
- def test_humidity_sensor(self):
- """Test humidity sensor."""
- device = mock_device(10, "Family Room", "Canary Pro")
- location = mock_location("Home")
+ 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
- data = Mock()
- data.get_reading.return_value = 50.4567
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "0.4"),
+ ]
- sensor = CanarySensor(data, SENSOR_TYPES[1], location, device)
- sensor.update()
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
- assert sensor.name == "Home Family Room Humidity"
- assert sensor.unit_of_measurement == PERCENTAGE
- assert sensor.state == 50.46
- assert sensor.icon == "mdi:water-percent"
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL
- 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")
+ instance.get_latest_readings.return_value = [
+ mock_reading("temperature", "21.12"),
+ mock_reading("humidity", "50.46"),
+ mock_reading("air_quality", "1.0"),
+ ]
- data = Mock()
- data.get_reading.return_value = 0.4
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ await hass.async_block_till_done()
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL
- 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
+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", {})
- 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")
+ registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
- data = Mock()
- data.get_reading.return_value = 0.59
+ online_device_at_home = mock_device(20, "Dining Room", True, "Canary Flex")
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
+ instance = canary.return_value
+ instance.get_locations.return_value = [
+ mock_location(100, "Home", True, devices=[online_device_at_home]),
+ ]
- 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"
+ instance.get_latest_readings.return_value = [
+ mock_reading("battery", "70.4567"),
+ mock_reading("wifi", "-57"),
+ ]
- air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
- assert air_quality == STATE_AIR_QUALITY_ABNORMAL
+ 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()
- 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")
+ sensors = {
+ "home_dining_room_battery": (
+ "20_battery",
+ "70.46",
+ PERCENTAGE,
+ DEVICE_CLASS_BATTERY,
+ None,
+ ),
+ "home_dining_room_wifi": (
+ "20_wifi",
+ "-57.0",
+ "dBm",
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ None,
+ ),
+ }
- data = Mock()
- data.get_reading.return_value = 1.0
+ 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]
- sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
- sensor.update()
+ 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]
- 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"
+ 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 2842ddc23f0..2fff760bb70 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.async_mock import patch
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 d0d9c4b25b7..aaabca71885 100644
--- a/tests/components/cloud/test_client.py
+++ b/tests/components/cloud/test_client.py
@@ -5,6 +5,7 @@ import pytest
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 test_webhook_msg(hass, caplog):
{
"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 test_webhook_msg(hass, caplog):
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 test_webhook_msg(hass, caplog):
{
"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 a9d9c61444a..c6d315b05b5 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
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+)
+from homeassistant.setup import async_setup_component
-# pylint: disable=invalid-name
-class TestCommandSwitch(unittest.TestCase):
- """Test the command switch."""
+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()
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
- 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()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == 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
+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()
- 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 = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
- state = self.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,
+ )
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
- state = self.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,
+ )
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
- 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()
+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 = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
- common.turn_on(self.hass, "switch.test")
- self.hass.block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- state = self.hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ state = 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()
+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 = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
+ state = hass.states.get("switch.test")
+ assert STATE_OFF == state.state
- common.turn_off(self.hass, "switch.test")
- self.hass.block_till_done()
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- state = self.hass.states.get("switch.test")
- assert STATE_ON == state.state
+ state = 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,
- ]
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
- no_state_device = command_line.CommandSwitch(*init_args)
- assert no_state_device.assumed_state
+ state = hass.states.get("switch.test")
+ assert STATE_ON == state.state
- # Set state command
- init_args[-3] = "cat {}"
- state_device = command_line.CommandSwitch(*init_args)
- assert not state_device.assumed_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,
+ ]
- 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,
- ]
+ no_state_device = command_line.CommandSwitch(*init_args)
+ assert no_state_device.assumed_state
- test_switch = command_line.CommandSwitch(*init_args)
- assert test_switch.entity_id == "switch.test_device_name"
- assert test_switch.name == "Test friendly name!"
+ # 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 2a696624e0c..d63d10437cc 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
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/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 30f7251e067..8a93c8a5cdb 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
DEVICE_CLASS_VIBRATION,
)
+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
@@ -182,3 +183,34 @@ async def test_add_new_binary_sensor(hass):
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."""
+ gateway = await setup_deconz_integration(
+ hass,
+ options={deconz.gateway.CONF_ALLOW_NEW_DEVICES: False},
+ )
+ 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, gateway.config_entry.entry_id
+ )
+ )
+ == 0
+ )
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index 43536a44bbe..104edca6cca 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -14,10 +14,11 @@ from homeassistant.components.deconz.config_flow import (
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
@@ -32,7 +33,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 +53,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 +74,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 +99,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 +126,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 +147,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(
@@ -201,7 +202,7 @@ async def test_manual_configuration_update_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(
@@ -222,13 +223,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(
@@ -247,7 +248,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 +269,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 +291,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 +312,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 +332,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 +375,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(
@@ -561,12 +562,17 @@ async def test_option_flow(hass):
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_deconz_event.py b/tests/components/deconz/test_deconz_event.py
index fc8d4f9d1ba..525821e389f 100644
--- a/tests/components/deconz/test_deconz_event.py
+++ b/tests/components/deconz/test_deconz_event.py
@@ -40,6 +40,14 @@ SENSORS = {
"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",
+ },
}
@@ -53,7 +61,7 @@ async def test_deconz_events(hass):
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
+ assert len(gateway.events) == 5
switch_1 = hass.states.get("sensor.switch_1")
assert switch_1 is None
@@ -101,6 +109,20 @@ async def test_deconz_events(hass):
"gesture": 0,
}
+ 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 gateway.async_reset()
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py
index 15ff304459b..64818401bcb 100644
--- a/tests/components/deconz/test_gateway.py
+++ b/tests/components/deconz/test_gateway.py
@@ -81,6 +81,7 @@ async def test_gateway_setup(hass):
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
diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py
index e880ea1000b..cfbc8cae56c 100644
--- a/tests/components/deconz/test_services.py
+++ b/tests/components/deconz/test_services.py
@@ -1,12 +1,16 @@
"""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.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 +47,17 @@ SENSOR = {
}
}
+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 +67,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 +88,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):
@@ -198,3 +213,75 @@ 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)
+ gateway = 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=gateway.config_entry.entry_id, identifiers={("mac", "123")}
+ )
+
+ assert (
+ len(
+ [
+ entry
+ for entry in device_registry.devices.values()
+ if gateway.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=gateway.config_entry,
+ device_id=device.id,
+ )
+
+ assert (
+ len(
+ async_entries_for_config_entry(
+ entity_registry, gateway.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 gateway.config_entry.entry_id in entry.config_entries
+ ]
+ )
+ == 3 # Gateway, light and switch
+ )
+
+ assert (
+ len(
+ async_entries_for_config_entry(
+ entity_registry, gateway.config_entry.entry_id
+ )
+ )
+ == 2 # Light and switch battery
+ )
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index 7edf1dc7d60..6bf9cc44c56 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -6,13 +6,6 @@ from homeassistant.setup import async_setup_component
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_geo_location.py b/tests/components/demo/test_geo_location.py
index 7ca870bc34f..ac32fff075f 100644
--- a/tests/components/demo/test_geo_location.py
+++ b/tests/components/demo/test_geo_location.py
@@ -8,7 +8,12 @@ from homeassistant.components.demo.geo_location import (
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 @@ class TestDemoPlatform(unittest.TestCase):
# ignore home zone state
continue
assert (
- abs(state.attributes["latitude"] - self.hass.config.latitude) < 1.0
- )
- assert (
- abs(state.attributes["longitude"] - self.hass.config.longitude)
+ abs(state.attributes[ATTR_LATITUDE] - self.hass.config.latitude)
< 1.0
)
- assert state.attributes["unit_of_measurement"] == LENGTH_KILOMETERS
+ assert (
+ abs(state.attributes[ATTR_LONGITUDE] - self.hass.config.longitude)
+ < 1.0
+ )
+ 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 1ab8195c4db..70b05cd25cc 100644
--- a/tests/components/demo/test_media_player.py
+++ b/tests/components/demo/test_media_player.py
@@ -3,6 +3,7 @@ import pytest
import voluptuous as vol
import homeassistant.components.media_player as mp
+from homeassistant.const import ATTR_SUPPORTED_FEATURES
from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION
from homeassistant.setup import async_setup_component
@@ -203,7 +204,7 @@ async def test_seek(hass, mock_media_seek):
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)
diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py
index 19715577e2a..4879be9d18c 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/directv/__init__.py b/tests/components/directv/__init__.py
index 6b71b1e8cfe..9f09c377bd4 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 26f79e3e62f..8003f83d996 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/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py
index cef4081c607..4f696d905d3 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 deea32d2e34..7df10fb08e8 100644
--- a/tests/components/dynalite/test_light.py
+++ b/tests/components/dynalite/test_light.py
@@ -4,6 +4,7 @@ from dynalite_devices_lib.light import DynaliteChannelLightDevice
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 7c0c5d632d3..de375e3b348 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 cca589875aa..77105dc73db 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 import DysonPureHotCool
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 import (
)
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 @@ from homeassistant.components.climate.const import (
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."""
+@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
- 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()
+@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()
- 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()
+ device = mocked_devices.return_value[0]
- 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
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ True,
+ )
- def test_setup_component(self):
- """Test setup component with devices."""
- device_fan = _get_device_heat_on()
- device_non_fan = _get_device_off()
+ set_config = device.set_configuration
+ assert set_config.call_args == call(heat_mode=HeatMode.HEAT_ON)
- def _add_device(devices):
- assert len(devices) == 1
- assert devices[0].name == "Device_name"
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_COOL},
+ True,
+ )
- self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan]
- dyson.setup_platform(self.hass, None, _add_device)
+ set_config = device.set_configuration
+ assert set_config.call_args == call(heat_mode=HeatMode.HEAT_OFF)
- 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()
+@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()
- 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)
- )
+ device = mocked_devices.return_value[0]
+ device.temp_unit = TEMP_CELSIUS
- # 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)
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_FOCUS},
+ True,
+ )
- 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)
- )
+ set_config = device.set_configuration
+ assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_ON)
- 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()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_DIFFUSE},
+ True,
+ )
- 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)
- )
+ set_config = device.set_configuration
+ assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_OFF)
- 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)
+@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()
- entity.set_fan_mode(dyson.FAN_DIFFUSE)
- set_config = device.set_configuration
- set_config.assert_called_with(focus_mode=FocusMode.FOCUS_OFF)
+ 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]
- 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
+ device = mocked_devices.return_value[0]
+ update_callback = device.add_message_listener.call_args[0][0]
- 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
+ device.state.focus_mode = FocusMode.FOCUS_ON.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
- 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
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes[ATTR_FAN_MODE] == FAN_FOCUS
- 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
+ device.state.focus_mode = FocusMode.FOCUS_OFF.value
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
- entity.set_hvac_mode(dyson.HVAC_MODE_HEAT)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON)
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes[ATTR_FAN_MODE] == FAN_DIFFUSE
- entity.set_hvac_mode(dyson.HVAC_MODE_COOL)
- set_config = device.set_configuration
- set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF)
+ 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()
- 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
+ state = hass.states.get("climate.temp_name")
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
- 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)
+ device.environmental_state.humidity = 0
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
- 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)
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
- 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
+ device.environmental_state = None
+ await hass.async_add_executor_job(update_callback, MockDysonState())
+ await hass.async_block_till_done()
- 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
+ state = hass.states.get("climate.temp_name")
+ assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
- 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
+ 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()
- 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()
+ state = hass.states.get("climate.temp_name")
+ assert state.state == HVAC_MODE_COOL
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
- 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
+@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()
- 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
+ entity_ids = hass.states.async_entity_ids(DOMAIN)
+ assert not entity_ids
- 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
+@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()
- 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
+ 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 807cf3565ed..11770e1f133 100644
--- a/tests/components/dyson/test_fan.py
+++ b/tests/components/dyson/test_fan.py
@@ -38,6 +38,10 @@ class MockDysonState(DysonPureCoolState):
"""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/test_sensor.py b/tests/components/eafm/test_sensor.py
index 6cce7a2bc4b..a7ee0403c7c 100644
--- a/tests/components/eafm/test_sensor.py
+++ b/tests/components/eafm/test_sensor.py
@@ -5,6 +5,7 @@ import aiohttp
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/elgato/__init__.py b/tests/components/elgato/__init__.py
index 5f0f2f5fb14..3b1942aee14 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 bd65811133a..e0d34aecad2 100644
--- a/tests/components/elgato/test_config_flow.py
+++ b/tests/components/elgato/test_config_flow.py
@@ -5,7 +5,7 @@ from homeassistant import data_entry_flow
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()
@@ -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 0f61178107d..576a464c86a 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.components.emulated_hue.hue_api import (
)
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 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client):
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 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client):
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 8a5556b1222..e68688399e0 100644
--- a/tests/components/emulated_hue/test_upnp.py
+++ b/tests/components/emulated_hue/test_upnp.py
@@ -9,7 +9,7 @@ import requests
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 @@ MX:3
)
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 @@ MX:3
)
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 3a835cb0547..907feb85569 100644
--- a/tests/components/flo/conftest.py
+++ b/tests/components/flo/conftest.py
@@ -5,7 +5,7 @@ import time
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 265f2ae2d38..edc9705b7cd 100644
--- a/tests/components/flo/test_config_flow.py
+++ b/tests/components/flo/test_config_flow.py
@@ -4,6 +4,7 @@ import time
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/flux/test_switch.py b/tests/components/flux/test_switch.py
index 594b3aff2b2..be1fcf4c5ee 100644
--- a/tests/components/flux/test_switch.py
+++ b/tests/components/flux/test_switch.py
@@ -20,7 +20,6 @@ from tests.common import (
async_mock_service,
mock_restore_cache,
)
-from tests.components.switch import common
async def test_valid_config(hass):
@@ -224,8 +223,12 @@ async def test_flux_before_sunrise(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]
@@ -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 @@ async def test_flux_after_sunrise_before_sunset(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]
@@ -389,8 +400,12 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time):
)
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 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time):
)
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 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time):
)
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 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time):
)
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 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(
)
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 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(
)
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 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(
)
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 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(
)
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 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time):
)
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 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time):
)
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 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time):
)
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 @@ async def test_flux_with_mired(hass, legacy_patchable_time):
)
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 @@ async def test_flux_with_rgb(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]
diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py
index 181c963ee4f..f655e727667 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.config_entries import (
)
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/frontend/test_init.py b/tests/components/frontend/test_init.py
index 7298ad753d2..7802ee60e8c 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 99ace50e77d..8bf1c6abe15 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 dd3132b0117..21b6830e7f4 100644
--- a/tests/components/geofency/test_init.py
+++ b/tests/components/geofency/test_init.py
@@ -6,6 +6,8 @@ from homeassistant.components import zone
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 00000000000..0ba2a912766
--- /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 00000000000..5a367c452c6
--- /dev/null
+++ b/tests/components/goalzero/test_config_flow.py
@@ -0,0 +1,120 @@
+"""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},
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+ _flow_next(hass, result["flow_id"])
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=CONF_CONFIG_FLOW,
+ )
+ 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 b1ab2284580..91bffca56ce 100644
--- a/tests/components/gogogate2/test_cover.py
+++ b/tests/components/gogogate2/test_cover.py
@@ -25,6 +25,7 @@ from homeassistant.components.gogogate2.const import (
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.setup import async_setup_component
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,
@@ -144,17 +109,17 @@ async def test_import(
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,
@@ -176,9 +141,9 @@ async def test_import(
voltage=40,
),
door2=ISmartGateDoor(
- door_id=1,
+ door_id=2,
permission=True,
- name=None,
+ name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
@@ -193,16 +158,16 @@ async def test_import(
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",
@@ -212,6 +177,50 @@ async def test_import(
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 = {
@@ -243,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:
@@ -312,7 +322,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None:
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,
@@ -364,71 +374,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None:
@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,
- voltage=40,
- ),
- 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,
- voltage=40,
- ),
- 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,
- voltage=40,
- ),
- 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
@@ -470,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/group/test_init.py b/tests/components/group/test_init.py
index 9684c107bb7..18d37267334 100644
--- a/tests/components/group/test_init.py
+++ b/tests/components/group/test_init.py
@@ -8,12 +8,15 @@ from homeassistant.const import (
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 @@ class TestComponentsGroup(unittest.TestCase):
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 @@ class TestComponentsGroup(unittest.TestCase):
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 @@ class TestComponentsGroup(unittest.TestCase):
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 @@ class TestComponentsGroup(unittest.TestCase):
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
+ 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()
- 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()
+ test_group = await group.Group.async_create_group(
+ hass, "init_group", ["light.Bowl", "light.Ceiling"], False
+ )
+ await hass.async_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
+ 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
- def test_modify_group(self):
- """Test modifying a group."""
- group_conf = OrderedDict()
- group_conf["modify_group"] = {"name": "friendly_name", "icon": "mdi:work"}
+ # Try on non existing state
+ assert not group.is_on(hass, "non.existing")
- 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()
+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/hassio/test_http.py b/tests/components/hassio/test_http.py
index 7386cf57d0c..d069b311ab2 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -3,6 +3,8 @@ import asyncio
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/history/test_init.py b/tests/components/history/test_init.py
index c15e4431f87..494ea89b3b8 100644
--- a/tests/components/history/test_init.py
+++ b/tests/components/history/test_init.py
@@ -18,7 +18,7 @@ from tests.common import (
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/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py
index 84b7e725f0a..0e4c2674b1b 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
@@ -38,11 +38,15 @@ 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()
@@ -66,8 +70,12 @@ async def test_if_fires_on_event_extra_data(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.bus.async_fire("test_event")
await hass.async_block_till_done()
diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py
index 932dde91120..09a13f95603 100644
--- a/tests/components/homeassistant/triggers/test_numeric_state.py
+++ b/tests/components/homeassistant/triggers/test_numeric_state.py
@@ -8,6 +8,7 @@ import homeassistant.components.automation as automation
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 @@ from tests.common import (
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 990fc9cc956..61fa991e0f4 100644
--- a/tests/components/homeassistant/triggers/test_state.py
+++ b/tests/components/homeassistant/triggers/test_state.py
@@ -5,6 +5,7 @@ import pytest
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 @@ from tests.common import (
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()
@@ -1232,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 3d32748c176..f428bcf29bc 100644
--- a/tests/components/homeassistant/triggers/test_time_pattern.py
+++ b/tests/components/homeassistant/triggers/test_time_pattern.py
@@ -6,12 +6,12 @@ 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
@@ -55,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()
diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py
index 79c15344b17..0cb31e1b701 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 bcbbbf3bcbf..ea91733fdab 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -24,6 +24,7 @@ from homeassistant.const import (
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 757281af1e9..c22d6286e76 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 3eed6d05816..cd193b61646 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_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index b139fac3657..d6bf74bb7cf 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_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py
new file mode 100644
index 00000000000..a9744fb7bfc
--- /dev/null
+++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py
@@ -0,0 +1,52 @@
+"""
+Regression tests for Aqara AR004.
+
+This device has a non-standard programmable stateless switch service that has a
+service-label-index despite not being linked to a service-label.
+
+https://github.com/home-assistant/core/pull/39090
+"""
+
+from tests.common import assert_lists_same, async_get_device_automations
+from tests.components.homekit_controller.common import (
+ setup_accessories_from_file,
+ setup_test_accessories,
+)
+
+
+async def test_aqara_switch_setup(hass):
+ """Test that a Aqara Switch can be correctly setup in HA."""
+ accessories = await setup_accessories_from_file(hass, "aqara_switch.json")
+ config_entry, pairing = await setup_test_accessories(hass, accessories)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ battery_id = "sensor.programmable_switch_battery"
+ battery = entity_registry.async_get(battery_id)
+ assert battery.unique_id == "homekit-111a1111a1a111-5"
+
+ # The fixture file has 1 button and a battery
+
+ expected = [
+ {
+ "device_id": battery.device_id,
+ "domain": "sensor",
+ "entity_id": "sensor.programmable_switch_battery",
+ "platform": "device",
+ "type": "battery_level",
+ }
+ ]
+
+ for subtype in ("single_press", "double_press", "long_press"):
+ expected.append(
+ {
+ "device_id": battery.device_id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": "button1",
+ "subtype": subtype,
+ }
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", battery.device_id)
+ assert_lists_same(triggers, expected)
diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
index 0b6ebc00eba..67b7508eb94 100644
--- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
+++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
@@ -1,5 +1,6 @@
"""Tests for handling accessories on a Hue bridge via HomeKit."""
+from tests.common import assert_lists_same, async_get_device_automations
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
@@ -34,3 +35,32 @@ async def test_hue_bridge_setup(hass):
assert device.name == "Hue dimmer switch"
assert device.model == "RWL021"
assert device.sw_version == "45.1.17846"
+
+ # The fixture file has 1 dimmer, which is a remote with 4 buttons
+ # It (incorrectly) claims to support single, double and long press events
+ # It also has a battery
+
+ expected = [
+ {
+ "device_id": device.id,
+ "domain": "sensor",
+ "entity_id": "sensor.hue_dimmer_switch_battery",
+ "platform": "device",
+ "type": "battery_level",
+ }
+ ]
+
+ for button in ("button1", "button2", "button3", "button4"):
+ for subtype in ("single_press", "double_press", "long_press"):
+ expected.append(
+ {
+ "device_id": device.id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": button,
+ "subtype": subtype,
+ }
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+ assert_lists_same(triggers, expected)
diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py
index acebac95006..cd3f57137bf 100644
--- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py
+++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py
@@ -1,12 +1,12 @@
"""Make sure that handling real world LG HomeKit characteristics isn't broken."""
-
from homeassistant.components.media_player.const import (
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_SELECT_SOURCE,
)
+from tests.common import async_get_device_automations
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
@@ -62,3 +62,7 @@ async def test_lg_tv(hass):
assert device.model == "OLED55B9PUA"
assert device.sw_version == "04.71.04"
assert device.via_device_id is None
+
+ # A TV doesn't have any triggers
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+ assert triggers == []
diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py
index 460d14d0d48..e9ba4420176 100644
--- a/tests/components/homekit_controller/test_binary_sensor.py
+++ b/tests/components/homekit_controller/test_binary_sensor.py
@@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_GAS,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
@@ -15,6 +16,7 @@ from tests.components.homekit_controller.common import setup_test_component
MOTION_DETECTED = ("motion", "motion-detected")
CONTACT_STATE = ("contact", "contact-state")
SMOKE_DETECTED = ("smoke", "smoke-detected")
+CARBON_MONOXIDE_DETECTED = ("carbon-monoxide", "carbon-monoxide.detected")
OCCUPANCY_DETECTED = ("occupancy", "occupancy-detected")
LEAK_DETECTED = ("leak", "leak-detected")
@@ -88,6 +90,29 @@ async def test_smoke_sensor_read_state(hass, utcnow):
assert state.attributes["device_class"] == DEVICE_CLASS_SMOKE
+def create_carbon_monoxide_sensor_service(accessory):
+ """Define carbon monoxide sensor characteristics."""
+ service = accessory.add_service(ServicesTypes.CARBON_MONOXIDE_SENSOR)
+
+ cur_state = service.add_char(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED)
+ cur_state.value = 0
+
+
+async def test_carbon_monoxide_sensor_read_state(hass, utcnow):
+ """Test that we can read the state of a HomeKit contact accessory."""
+ helper = await setup_test_component(hass, create_carbon_monoxide_sensor_service)
+
+ helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 0
+ state = await helper.poll_and_get_state()
+ assert state.state == "off"
+
+ helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 1
+ state = await helper.poll_and_get_state()
+ assert state.state == "on"
+
+ assert state.attributes["device_class"] == DEVICE_CLASS_GAS
+
+
def create_occupancy_sensor_service(accessory):
"""Define occupancy characteristics."""
service = accessory.add_service(ServicesTypes.OCCUPANCY_SENSOR)
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index e5c8e381a5f..a8eb869abf4 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -52,14 +52,17 @@ INVALID_PAIRING_CODES = [
"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 ",
]
@@ -548,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/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py
new file mode 100644
index 00000000000..c8ef2cbef38
--- /dev/null
+++ b/tests/components/homekit_controller/test_device_trigger.py
@@ -0,0 +1,298 @@
+"""Test homekit_controller stateless triggers."""
+from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.homekit_controller.const import DOMAIN
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+)
+from tests.components.homekit_controller.common import setup_test_component
+
+
+# pylint: disable=redefined-outer-name
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+def create_remote(accessory):
+ """Define characteristics for a button (that is inn a group)."""
+ service_label = accessory.add_service(ServicesTypes.SERVICE_LABEL)
+
+ char = service_label.add_char(CharacteristicsTypes.SERVICE_LABEL_NAMESPACE)
+ char.value = 1
+
+ for i in range(4):
+ button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH)
+ button.linked.append(service_label)
+
+ char = button.add_char(CharacteristicsTypes.INPUT_EVENT)
+ char.value = 0
+ char.perms = ["pw", "pr", "ev"]
+
+ char = button.add_char(CharacteristicsTypes.NAME)
+ char.value = f"Button {i + 1}"
+
+ char = button.add_char(CharacteristicsTypes.SERVICE_LABEL_INDEX)
+ char.value = i
+
+ battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE)
+ battery.add_char(CharacteristicsTypes.BATTERY_LEVEL)
+
+
+def create_button(accessory):
+ """Define a button (that is not in a group)."""
+ button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH)
+
+ char = button.add_char(CharacteristicsTypes.INPUT_EVENT)
+ char.value = 0
+ char.perms = ["pw", "pr", "ev"]
+
+ char = button.add_char(CharacteristicsTypes.NAME)
+ char.value = "Button 1"
+
+ battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE)
+ battery.add_char(CharacteristicsTypes.BATTERY_LEVEL)
+
+
+def create_doorbell(accessory):
+ """Define a button (that is not in a group)."""
+ button = accessory.add_service(ServicesTypes.DOORBELL)
+
+ char = button.add_char(CharacteristicsTypes.INPUT_EVENT)
+ char.value = 0
+ char.perms = ["pw", "pr", "ev"]
+
+ char = button.add_char(CharacteristicsTypes.NAME)
+ char.value = "Doorbell"
+
+ battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE)
+ battery.add_char(CharacteristicsTypes.BATTERY_LEVEL)
+
+
+async def test_enumerate_remote(hass, utcnow):
+ """Test that remote is correctly enumerated."""
+ await setup_test_component(hass, create_remote)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get("sensor.testdevice_battery")
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entry.device_id)
+
+ expected = [
+ {
+ "device_id": device.id,
+ "domain": "sensor",
+ "entity_id": "sensor.testdevice_battery",
+ "platform": "device",
+ "type": "battery_level",
+ }
+ ]
+
+ for button in ("button1", "button2", "button3", "button4"):
+ for subtype in ("single_press", "double_press", "long_press"):
+ expected.append(
+ {
+ "device_id": device.id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": button,
+ "subtype": subtype,
+ }
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+ assert_lists_same(triggers, expected)
+
+
+async def test_enumerate_button(hass, utcnow):
+ """Test that a button is correctly enumerated."""
+ await setup_test_component(hass, create_button)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get("sensor.testdevice_battery")
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entry.device_id)
+
+ expected = [
+ {
+ "device_id": device.id,
+ "domain": "sensor",
+ "entity_id": "sensor.testdevice_battery",
+ "platform": "device",
+ "type": "battery_level",
+ }
+ ]
+
+ for subtype in ("single_press", "double_press", "long_press"):
+ expected.append(
+ {
+ "device_id": device.id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": "button1",
+ "subtype": subtype,
+ }
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+ assert_lists_same(triggers, expected)
+
+
+async def test_enumerate_doorbell(hass, utcnow):
+ """Test that a button is correctly enumerated."""
+ await setup_test_component(hass, create_doorbell)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get("sensor.testdevice_battery")
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entry.device_id)
+
+ expected = [
+ {
+ "device_id": device.id,
+ "domain": "sensor",
+ "entity_id": "sensor.testdevice_battery",
+ "platform": "device",
+ "type": "battery_level",
+ }
+ ]
+
+ for subtype in ("single_press", "double_press", "long_press"):
+ expected.append(
+ {
+ "device_id": device.id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": "doorbell",
+ "subtype": subtype,
+ }
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+ assert_lists_same(triggers, expected)
+
+
+async def test_handle_events(hass, utcnow, calls):
+ """Test that events are handled."""
+ helper = await setup_test_component(hass, create_remote)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get("sensor.testdevice_battery")
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entry.device_id)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "alias": "single_press",
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device.id,
+ "type": "button1",
+ "subtype": "single_press",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": (
+ "{{ trigger.platform}} - "
+ "{{ trigger.type }} - {{ trigger.subtype }}"
+ )
+ },
+ },
+ },
+ {
+ "alias": "long_press",
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device.id,
+ "type": "button2",
+ "subtype": "long_press",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": (
+ "{{ trigger.platform}} - "
+ "{{ trigger.type }} - {{ trigger.subtype }}"
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+
+ # Make sure first automation (only) fires for single press
+ helper.pairing.testing.update_named_service(
+ "Button 1", {CharacteristicsTypes.INPUT_EVENT: 0}
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "device - button1 - single_press"
+
+ # Make sure automation doesn't trigger for long press
+ helper.pairing.testing.update_named_service(
+ "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1}
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ # Make sure automation doesn't trigger for double press
+ helper.pairing.testing.update_named_service(
+ "Button 1", {CharacteristicsTypes.INPUT_EVENT: 2}
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ # Make sure second automation fires for long press
+ helper.pairing.testing.update_named_service(
+ "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2}
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "device - button2 - long_press"
+
+ # Turn the automations off
+ await hass.services.async_call(
+ "automation",
+ "turn_off",
+ {"entity_id": "automation.long_press"},
+ blocking=True,
+ )
+
+ await hass.services.async_call(
+ "automation",
+ "turn_off",
+ {"entity_id": "automation.single_press"},
+ blocking=True,
+ )
+
+ # Make sure event no longer fires
+ helper.pairing.testing.update_named_service(
+ "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2}
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 2
diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py
index f15b2b56a95..6d6ba84e243 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 f7999b5f015..c47f0bf25ea 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 fe7283b471e..20c5c41a5b5 100644
--- a/tests/components/homematicip_cloud/test_sensor.py
+++ b/tests/components/homematicip_cloud/test_sensor.py
@@ -24,6 +24,8 @@ from homeassistant.components.homematicip_cloud.sensor import (
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/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py
index 1e37ff2e021..bd3e955d2d8 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 00000000000..e4c1ee67efa
--- /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 00000000000..c400e34db51
--- /dev/null
+++ b/tests/components/hyperion/test_light.py
@@ -0,0 +1,430 @@
+"""Tests for the Hyperion integration."""
+# from tests.async_mock import AsyncMock, MagicMock, patch
+from asynctest import CoroutineMock, Mock, call, patch
+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
+
+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 = CoroutineMock(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 = CoroutineMock(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 = CoroutineMock(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 = CoroutineMock(return_value=True)
+ client.async_send_set_adjustment = CoroutineMock(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 = CoroutineMock(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 = CoroutineMock(return_value=True)
+ client.async_send_set_adjustment = CoroutineMock(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 = CoroutineMock(return_value=True)
+ client.async_send_set_component = CoroutineMock(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 = CoroutineMock(return_value=True)
+ client.async_send_set_effect = CoroutineMock(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 = CoroutineMock(return_value=True)
+ client.async_send_set_effect = CoroutineMock(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 = CoroutineMock(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 = CoroutineMock(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/influxdb/test_init.py b/tests/components/influxdb/test_init.py
index ca4e56ff54d..edb85e7b98d 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/light/test_init.py b/tests/components/light/test_init.py
index a3f24bef3c9..e723ac77e03 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 @@ from homeassistant.const import (
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
+from tests.common import async_mock_service
-class TestLight(unittest.TestCase):
- """Test the light module."""
+@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)
- # 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)
+ if os.path.isfile(user_light_file):
+ os.remove(user_light_file)
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
+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")
- if os.path.isfile(user_light_file):
- os.remove(user_light_file)
+ hass.states.async_set("light.test", STATE_OFF)
+ assert not light.is_on(hass, "light.test")
- 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")
+ # Test turn_on
+ turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
- self.hass.states.set("light.test", STATE_OFF)
- assert not light.is_on(self.hass, "light.test")
+ 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 turn_on
- turn_on_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ assert len(turn_on_calls) == 1
+ call = turn_on_calls[-1]
- 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",
- )
+ 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(turn_on_calls)
- call = turn_on_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_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)
+ assert len(turn_off_calls) == 1
+ call = turn_off_calls[-1]
- # Test turn_off
- turn_off_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_OFF)
+ 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"
- common.turn_off(
- self.hass, entity_id="entity_id_val", transition="transition_val"
- )
+ # Test toggle
+ toggle_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TOGGLE)
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: "entity_id_val", light.ATTR_TRANSITION: "transition_val"},
+ blocking=True,
+ )
- assert 1 == len(turn_off_calls)
- call = turn_off_calls[-1]
+ assert len(toggle_calls) == 1
+ call = toggle_calls[-1]
- 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]
+ 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"
- # Test toggle
- toggle_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TOGGLE)
- common.toggle(self.hass, entity_id="entity_id_val", transition="transition_val")
+async def test_services(hass):
+ """Test the provided services."""
+ platform = getattr(hass.components, "test.light")
- self.hass.block_till_done()
+ platform.init()
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
- assert 1 == len(toggle_calls)
- call = toggle_calls[-1]
+ ent1, ent2, ent3 = platform.ENTITIES
- 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]
+ # 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)
- def test_services(self):
- """Test the provided services."""
- platform = getattr(self.hass.components, "test.light")
+ # 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
+ )
- platform.init()
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
- self.hass.block_till_done()
+ assert not light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
- ent1, ent2, ent3 = platform.ENTITIES
+ # turn on all lights
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, 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 light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
+ assert light.is_on(hass, ent3.entity_id)
- # 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)
+ # turn off all lights
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
- self.hass.block_till_done()
+ 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)
- assert not light.is_on(self.hass, ent1.entity_id)
- assert light.is_on(self.hass, ent2.entity_id)
+ # 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,
+ )
- # turn on all lights
- common.turn_on(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()
+ # toggle all lights
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
+ )
- 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)
+ assert light.is_on(hass, ent1.entity_id)
+ assert light.is_on(hass, ent2.entity_id)
+ assert light.is_on(hass, ent3.entity_id)
- # turn off all lights
- common.turn_off(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 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)
- 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 off all lights by setting brightness to 0
- common.turn_on(self.hass)
-
- self.hass.block_till_done()
-
- common.turn_on(self.hass, brightness=0)
-
- self.hass.block_till_done()
-
- 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)
-
- # toggle all lights
- common.toggle(self.hass)
-
- self.hass.block_till_done()
-
- 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
- common.toggle(self.hass)
-
- self.hass.block_till_done()
-
- 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)
-
- # 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))
-
- self.hass.block_till_done()
-
- _, data = ent1.last_call("turn_on")
- assert {
+ # 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_HS_COLOR: (240, 100),
- } == data
+ 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,
+ )
- _, data = ent2.last_call("turn_on")
- assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data
+ _, data = ent1.last_call("turn_on")
+ assert data == {
+ light.ATTR_TRANSITION: 10,
+ light.ATTR_BRIGHTNESS: 20,
+ light.ATTR_HS_COLOR: (240, 100),
+ }
- _, data = ent3.last_call("turn_on")
- assert {light.ATTR_HS_COLOR: (71.059, 100)} == data
+ _, data = ent2.last_call("turn_on")
+ assert data == {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255}
- # 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 = ent3.last_call("turn_on")
+ assert data == {light.ATTR_HS_COLOR: (71.059, 100)}
- self.hass.block_till_done()
+ # 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: 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,
+ )
- 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)
- _, data = ent1.last_call("turn_off")
- assert {light.ATTR_TRANSITION: 10} == data
+ _, data = ent1.last_call("turn_off")
+ assert data == {light.ATTR_TRANSITION: 10}
- _, data = ent2.last_call("turn_off")
- assert {} == data
+ _, data = ent2.last_call("turn_off")
+ assert data == {}
- _, data = ent3.last_call("turn_off")
- assert {} == data
+ _, data = ent3.last_call("turn_off")
+ assert data == {}
- # One of the light profiles
- prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0
+ # One of the light profiles
+ prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0
- # 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
- )
-
- self.hass.block_till_done()
-
- _, 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 {
+ # 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_HS_COLOR: (prof_h, prof_s),
light.ATTR_TRANSITION: 1,
- } == data
+ },
+ blocking=True,
+ )
- # 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
+ _, 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,
+ }
- # 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])
+ _, data = ent2.last_call("turn_on")
+ assert data == {
+ light.ATTR_BRIGHTNESS: 100,
+ light.ATTR_HS_COLOR: (prof_h, prof_s),
+ light.ATTR_TRANSITION: 1,
+ }
- self.hass.block_till_done()
+ # 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,
+ )
- _, data = ent1.last_call("turn_on")
- assert {} == data
+ _, 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,
+ }
- _, 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"
+ # 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,
)
- 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 = 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()
-
- user_light_file = self.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")
-
- assert not setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ 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,
)
- def test_light_profiles(self):
- """Test light profiles."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
+ _, data = ent1.last_call("turn_on")
+ assert data == {}
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
+ _, data = ent2.last_call("turn_on")
+ assert data == {}
- 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")
+ _, data = ent3.last_call("turn_on")
+ assert data == {}
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ # 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,
)
- self.hass.block_till_done()
-
- ent1, _, _ = platform.ENTITIES
-
- common.turn_on(self.hass, ent1.entity_id, profile="test")
-
- self.hass.block_till_done()
-
- _, 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: 0,
- } == data
-
- common.turn_on(self.hass, ent1.entity_id, profile="test_off")
-
- self.hass.block_till_done()
-
- _, data = ent1.last_call("turn_off")
-
- assert not light.is_on(self.hass, ent1.entity_id)
- assert {light.ATTR_TRANSITION: 0} == data
-
- def test_light_profiles_with_transition(self):
- """Test light profiles with transition."""
- platform = getattr(self.hass.components, "test.light")
- platform.init()
-
- user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE)
-
- 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")
-
- assert setup_component(
- self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ 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,
)
- self.hass.block_till_done()
-
- ent1, _, _ = platform.ENTITIES
-
- common.turn_on(self.hass, ent1.entity_id, profile="test")
-
- self.hass.block_till_done()
-
- _, 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
-
- common.turn_on(self.hass, ent1.entity_id, profile="test_off")
-
- self.hass.block_till_done()
-
- _, data = ent1.last_call("turn_off")
-
- 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()
-
- 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\ngroup.all_lights.default,.4,.6,99,2\n"
+ 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,
)
- 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
+ _, data = ent1.last_call("turn_on")
+ assert 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()
+ _, data = ent2.last_call("turn_on")
+ assert data == {}
- 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)
+async def test_broken_light_profiles(hass, mock_storage):
+ """Test light profiles."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
- def _mock_open(path, *args, **kwargs):
- if path == user_light_file:
- return StringIO(profile_data)
- return real_open(path, *args, **kwargs)
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
- 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"
+ # 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 await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+
+
+async def test_light_profiles(hass, mock_storage):
+ """Test light profiles."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
+
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
+
+ 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")
+
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
+
+ ent1, _, _ = platform.ENTITIES
+
+ 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")
+ 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,
+ }
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"},
+ blocking=True,
+ )
+
+ _, data = ent1.last_call("turn_off")
+ assert not light.is_on(hass, ent1.entity_id)
+ assert data == {light.ATTR_TRANSITION: 0}
+
+
+async def test_light_profiles_with_transition(hass, mock_storage):
+ """Test light profiles with transition."""
+ platform = getattr(hass.components, "test.light")
+ platform.init()
+
+ user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE)
+
+ 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")
+
+ assert await async_setup_component(
+ hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
+ )
+ await hass.async_block_till_done()
+
+ ent1, _, _ = platform.ENTITIES
+
+ 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")
+ 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,
+ }
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"},
+ blocking=True,
+ )
+
+ _, data = ent1.last_call("turn_off")
+ assert not light.is_on(hass, ent1.entity_id)
+ assert data == {light.ATTR_TRANSITION: 0}
+
+
+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()
+
+ 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"}}
)
- 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()
+ await hass.async_block_till_done()
- dev = next(
- filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)
+ ent, _, _ = platform.ENTITIES
+ await hass.services.async_call(
+ light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True
+ )
+
+ _, data = ent.last_call("turn_on")
+ assert data == {
+ light.ATTR_HS_COLOR: (71.059, 100),
+ light.ATTR_BRIGHTNESS: 99,
+ light.ATTR_TRANSITION: 2,
+ }
+
+
+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 = 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
+ ):
+ 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 5e41f0bce89..96a5634d350 100644
--- a/tests/components/logbook/test_init.py
+++ b/tests/components/logbook/test_init.py
@@ -9,7 +9,7 @@ import unittest
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 @@ from homeassistant.const import (
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 @@ class TestComponentLogbook(unittest.TestCase):
# 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 @@ class TestComponentLogbook(unittest.TestCase):
)
assert len(entries) == 2
- self.assert_entry(
- entries[0], pointB, "bla", domain="sensor", entity_id=entity_id
- )
+ self.assert_entry(entries[0], pointB, "bla", 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)
-
- 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,628 +206,7 @@ class TestComponentLogbook(unittest.TestCase):
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
- )
-
- 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()
- 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
- )
- 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"
-
- # 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"
-
- 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)
-
- # 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
- )
- 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)"
+ self.assert_entry(entries[1], pointA, "bla", entity_id=entity_id)
def test_process_custom_logbook_entries(self):
"""Test if custom log book entries get added as an entry."""
@@ -1268,29 +234,14 @@ class TestComponentLogbook(unittest.TestCase):
)
assert len(entries) == 1
- self.assert_entry(
- entries[0], name=name, message=message, domain="sun", entity_id=entity_id
- )
+ self.assert_entry(entries[0], name=name, message=message, 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"]
+ return _assert_entry(entry, when, name, message, domain, entity_id)
def create_state_changed_event(
self,
@@ -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 @@ async def test_logbook_describe_event(hass, hass_client):
):
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 @@ async def test_exclude_described_event(hass, hass_client):
"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 @@ async def test_exclude_described_event(hass, hass_client):
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()
@@ -1991,7 +942,6 @@ async def test_logbook_entity_context_id(hass, hass_client):
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"
@@ -2150,11 +1100,93 @@ async def test_logbook_entity_matches_only(hass, hass_client):
assert len(json_dict) == 2
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"
+
+
+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(
+ 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)
+ 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(
+ id="9c5bd62de45711eaaeb351041eec8dd9",
+ user_id="9400facee45711eaa9308bfd3d19e474",
+ )
+ 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)
+ 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}&entity=switch.test_state,light.test_state&entity_matches_only"
+ )
+ assert response.status == 200
+ json_dict = await response.json()
+
+ assert len(json_dict) == 4
+
+ assert json_dict[0]["entity_id"] == "switch.test_state"
+
+ 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 ee2c9be377f..ba0072bc2f8 100644
--- a/tests/components/media_player/test_reproduce_state.py
+++ b/tests/components/media_player/test_reproduce_state.py
@@ -7,7 +7,6 @@ from homeassistant.components.media_player.const import (
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.components.media_player.reproduce_state import async_reproduc
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 @@ ENTITY_2 = "media_player.test2"
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/modbus/conftest.py b/tests/components/modbus/conftest.py
index 885aa5fc235..66f4d542525 100644
--- a/tests/components/modbus/conftest.py
+++ b/tests/components/modbus/conftest.py
@@ -6,14 +6,13 @@ 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,31 +37,40 @@ 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 run_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ data_array,
+ register_type,
+ entity_domain,
+ register_words,
+ expected,
):
- """Run test for given config and check that sensor outputs expected result."""
+ """Run test 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:
+ 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:
+ else: # CALL_TYPE_REGISTER_HOLDING
use_mock_hub.read_holding_registers.return_value = read_result
# Initialize sensor
@@ -76,8 +84,3 @@ async def run_test(
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 00000000000..ff64ad8723a
--- /dev/null
+++ b/tests/components/modbus/test_modbus_binary_sensor.py
@@ -0,0 +1,98 @@
+"""The tests for the Modbus sensor component."""
+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_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"
+ entity_domain = SENSOR_DOMAIN
+ data_array = {
+ CONF_INPUTS: [
+ dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **register_config)
+ ]
+ }
+ await run_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ data_array,
+ register_config.get(CONF_INPUT_TYPE),
+ entity_domain,
+ value,
+ expected,
+ )
+
+ # Check state
+ entity_id = f"{entity_domain}.{sensor_name}"
+ state = hass.states.get(entity_id).state
+ assert state == expected
+
+
+async def test_coil_true(hass, mock_hub):
+ """Test conversion of single word register."""
+ 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 ab4d745dc50..5ade2f197dd 100644
--- a/tests/components/modbus/test_modbus_sensor.py
+++ b/tests/components/modbus/test_modbus_sensor.py
@@ -8,7 +8,9 @@ from homeassistant.components.modbus.const import (
CONF_DATA_TYPE,
CONF_OFFSET,
CONF_PRECISION,
+ CONF_REGISTER,
CONF_REGISTER_TYPE,
+ CONF_REGISTERS,
CONF_REVERSE_ORDER,
CONF_SCALE,
DATA_TYPE_FLOAT,
@@ -17,12 +19,42 @@ from homeassistant.components.modbus.const import (
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_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"
+ entity_domain = SENSOR_DOMAIN
+ data_array = {
+ CONF_REGISTERS: [
+ dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config)
+ ]
+ }
+
+ await run_base_test(
+ sensor_name,
+ hass,
+ use_mock_hub,
+ data_array,
+ register_config.get(CONF_REGISTER_TYPE),
+ entity_domain,
+ register_words,
+ expected,
+ )
+
+ # Check state
+ entity_id = f"{entity_domain}.{sensor_name}"
+ state = hass.states.get(entity_id).state
+ assert state == expected
+
+
async def test_simple_word_register(hass, mock_hub):
"""Test conversion of single word register."""
register_config = {
@@ -32,11 +64,10 @@ async def test_simple_word_register(hass, mock_hub):
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",
)
@@ -45,11 +76,10 @@ async def test_simple_word_register(hass, mock_hub):
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",
)
@@ -64,11 +94,10 @@ async def test_offset(hass, mock_hub):
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",
)
@@ -83,11 +112,10 @@ async def test_scale_and_offset(hass, mock_hub):
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",
)
@@ -102,11 +130,10 @@ async def test_ints_can_have_precision(hass, mock_hub):
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",
)
@@ -121,11 +148,10 @@ async def test_floats_get_rounded_correctly(hass, mock_hub):
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",
)
@@ -140,11 +166,10 @@ async def test_parameters_as_strings(hass, mock_hub):
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",
)
@@ -159,11 +184,10 @@ async def test_floating_point_scale(hass, mock_hub):
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",
)
@@ -178,11 +202,10 @@ async def test_floating_point_offset(hass, mock_hub):
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",
)
@@ -197,11 +220,10 @@ async def test_signed_two_word_register(hass, mock_hub):
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",
)
@@ -216,11 +238,10 @@ async def test_unsigned_two_word_register(hass, mock_hub):
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),
)
@@ -233,11 +254,10 @@ async def test_reversed(hass, mock_hub):
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),
)
@@ -252,11 +272,10 @@ async def test_four_word_register(hass, mock_hub):
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",
)
@@ -271,11 +290,10 @@ async def test_four_word_register_precision_is_intact_with_int_params(hass, mock
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",
)
@@ -290,11 +308,10 @@ async def test_four_word_register_precision_is_lost_with_float_params(hass, mock
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",
)
@@ -310,11 +327,10 @@ async def test_two_word_input_register(hass, mock_hub):
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),
)
@@ -330,11 +346,10 @@ async def test_two_word_holding_register(hass, mock_hub):
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),
)
@@ -350,11 +365,10 @@ async def test_float_data_type(hass, mock_hub):
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",
)
@@ -370,11 +384,10 @@ async def test_string_data_type(hass, mock_hub):
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",
)
diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py
new file mode 100644
index 00000000000..fa041fb58ad
--- /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 f0dd76ff1b4..13e96c69adc 100644
--- a/tests/components/mqtt/test_trigger.py
+++ b/tests/components/mqtt/test_trigger.py
@@ -4,10 +4,10 @@ from unittest import mock
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/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py
index 1773c0d83da..fd36c57dfd1 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.media_source.models import PlayMedia
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,67 +18,9 @@ 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",
- "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",
- },
- }
- }
+ 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",
diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py
index 52064d1a92b..c7f15068d1d 100644
--- a/tests/components/nightscout/__init__.py
+++ b/tests/components/nightscout/__init__.py
@@ -22,6 +22,11 @@ SERVER_STATUS = ServerStatus.new_from_json_dict(
'{"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 a5f3315fbb1..71983f1b29d 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 homeassistant.const import CONF_URL
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/nut/test_sensor.py b/tests/components/nut/test_sensor.py
index 42fdc09e2b1..4950d467d6a 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 9d4acd7dde1..44b5193d79c 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 2604c6f39ac..bd8d81a4b0f 100644
--- a/tests/components/nws/test_weather.py
+++ b/tests/components/nws/test_weather.py
@@ -5,7 +5,11 @@ import aiohttp
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 @@ async def test_error_observation(hass, mock_simple_nws):
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 @@ async def test_error_observation(hass, mock_simple_nws):
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 @@ async def test_error_observation(hass, mock_simple_nws):
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 @@ async def test_error_observation(hass, mock_simple_nws):
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 8a36b299d87..27dc47a6df4 100644
--- a/tests/components/nzbget/__init__.py
+++ b/tests/components/nzbget/__init__.py
@@ -26,6 +26,8 @@ ENTRY_CONFIG = {
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_VERSION = "21.0"
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 @@ MOCK_HISTORY = [
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 00000000000..5855253b1d1
--- /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 362ba25ff67..a58d1faa766 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 62532c56699..d24a33d1f5b 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 43803384740..8bbee0047a9 100644
--- a/tests/components/nzbget/test_sensor.py
+++ b/tests/components/nzbget/test_sensor.py
@@ -14,7 +14,7 @@ from . import init_integration
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):
@@ -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 00000000000..c12fe8ca526
--- /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 00000000000..b7b8008abaa
--- /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 00000000000..ef29ff9f674
--- /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 0d425642622..a1b857a52ce 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/ozw/test_climate.py b/tests/components/ozw/test_climate.py
index 70fba99f7f2..3414e6c4832 100644
--- a/tests/components/ozw/test_climate.py
+++ b/tests/components/ozw/test_climate.py
@@ -57,6 +57,24 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
assert round(msg["payload"]["Value"], 2) == 78.98
assert msg["payload"]["ValueIDKey"] == 281475099443218
+ # Test hvac_mode with set_temperature
+ await hass.services.async_call(
+ "climate",
+ "set_temperature",
+ {
+ "entity_id": "climate.ct32_thermostat_mode",
+ "temperature": 24.1,
+ "hvac_mode": "cool",
+ },
+ blocking=True,
+ )
+ assert len(sent_messages) == 3 # 2 messages
+ msg = sent_messages[-1]
+ assert msg["topic"] == "OpenZWave/1/command/setvalue/"
+ # Celsius is converted to Fahrenheit here!
+ assert round(msg["payload"]["Value"], 2) == 75.38
+ assert msg["payload"]["ValueIDKey"] == 281475099443218
+
# Test set mode
await hass.services.async_call(
"climate",
@@ -64,7 +82,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
{"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_HEAT_COOL},
blocking=True,
)
- assert len(sent_messages) == 2
+ assert len(sent_messages) == 4
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412}
@@ -76,7 +94,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
{"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"},
blocking=True,
)
- assert len(sent_messages) == 2
+ assert len(sent_messages) == 4
assert "Received an invalid hvac mode: fan_only" in caplog.text
# Test set fan mode
@@ -86,7 +104,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
{"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"},
blocking=True,
)
- assert len(sent_messages) == 3
+ assert len(sent_messages) == 5
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948}
@@ -98,7 +116,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
{"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"},
blocking=True,
)
- assert len(sent_messages) == 3
+ assert len(sent_messages) == 5
assert "Received an invalid fan mode: invalid fan mode" in caplog.text
# Test incoming mode change to auto,
@@ -123,7 +141,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 5 # 2 messages !
+ assert len(sent_messages) == 7 # 2 messages !
msg = sent_messages[-2] # low setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert round(msg["payload"]["Value"], 2) == 68.0
@@ -162,7 +180,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 6
+ assert len(sent_messages) == 8
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
@@ -180,7 +198,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 7
+ assert len(sent_messages) == 9
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
@@ -199,7 +217,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 8
+ assert len(sent_messages) == 10
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
@@ -217,7 +235,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 8
+ assert len(sent_messages) == 10
assert "Received an invalid preset mode: invalid preset mode" in caplog.text
# test thermostat device without a mode commandclass
@@ -244,7 +262,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 9
+ assert len(sent_messages) == 11
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
@@ -261,5 +279,49 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
},
blocking=True,
)
- assert len(sent_messages) == 9
+ 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/plant/test_init.py b/tests/components/plant/test_init.py
index 866702488dc..f30350141c4 100644
--- a/tests/components/plant/test_init.py
+++ b/tests/components/plant/test_init.py
@@ -8,6 +8,7 @@ import homeassistant.components.plant as plant
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 00000000000..4e59b551574
--- /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 8055ab0d5b1..a20d70fbb7e 100644
--- a/tests/components/plex/helpers.py
+++ b/tests/components/plex/helpers.py
@@ -1,7 +1,8 @@
"""Helper methods for Plex tests."""
+from plexwebsocket import SIGNAL_DATA
def trigger_plex_update(mock_websocket):
"""Call the websocket callback method."""
callback = mock_websocket.call_args[0][1]
- callback()
+ callback(SIGNAL_DATA, None, None)
diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py
index 9cf6d7a7332..d96fdd4a00b 100644
--- a/tests/components/plex/test_browse_media.py
+++ b/tests/components/plex/test_browse_media.py
@@ -3,39 +3,16 @@ from homeassistant.components.media_player.const import (
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 1bd2ce82863..476c342f176 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -24,6 +24,7 @@ from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
SOURCE_INTEGRATION_DISCOVERY,
+ SOURCE_REAUTH,
)
from homeassistant.const import (
CONF_HOST,
@@ -34,7 +35,7 @@ from homeassistant.const import (
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 666a819e8ca..3c4f9031fad 100644
--- a/tests/components/plex/test_init.py
+++ b/tests/components/plex/test_init.py
@@ -24,27 +24,8 @@ from tests.async_mock import patch
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 @@ async def test_setup_when_certificate_changed(hass):
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 @@ async def test_setup_when_certificate_changed(hass):
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 d3e2de91cf9..fdacd6051ae 100644
--- a/tests/components/plex/test_media_players.py
+++ b/tests/components/plex/test_media_players.py
@@ -3,32 +3,12 @@ from plexapi.exceptions import NotFound
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 b031aff25cd..bd694419421 100644
--- a/tests/components/plex/test_playback.py
+++ b/tests/components/plex/test_playback.py
@@ -10,32 +10,11 @@ from homeassistant.components.plex.const import DOMAIN, SERVERS, SERVICE_PLAY_ON
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 b3623681f8a..b650821b3f2 100644
--- a/tests/components/plex/test_server.py
+++ b/tests/components/plex/test_server.py
@@ -40,33 +40,16 @@ from .mock_classes import (
)
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 078ba3b97e9..a3f4d4c833a 100644
--- a/tests/components/plex/test_services.py
+++ b/tests/components/plex/test_services.py
@@ -15,31 +15,15 @@ from homeassistant.const import (
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/test_config_flow.py b/tests/components/plugwise/test_config_flow.py
index e0f4993df55..9b67f06e469 100644
--- a/tests/components/plugwise/test_config_flow.py
+++ b/tests/components/plugwise/test_config_flow.py
@@ -3,7 +3,11 @@ from Plugwise_Smile.Smile import Smile
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 @@ from tests.common import MockConfigEntry
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,6 +114,7 @@ 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
@@ -176,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/prometheus/test_init.py b/tests/components/prometheus/test_init.py
index e04cbf9e632..f187429b151 100644
--- a/tests/components/prometheus/test_init.py
+++ b/tests/components/prometheus/test_init.py
@@ -9,6 +9,7 @@ from homeassistant.components.demo.sensor import DemoSensor
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/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py
index 681b3675aa6..a16923d0a73 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 @@ FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json"
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 57861b8b72b..6dae784a0cc 100644
--- a/tests/components/pvpc_hourly_pricing/test_sensor.py
+++ b/tests/components/pvpc_hourly_pricing/test_sensor.py
@@ -54,12 +54,8 @@ async def test_sensor_availability(
# 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/recorder/test_init.py b/tests/components/recorder/test_init.py
index 6116b383341..08af027bce3 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -181,7 +181,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/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py
index e56342d861f..b18d8f300cf 100644
--- a/tests/components/rest/test_binary_sensor.py
+++ b/tests/components/rest/test_binary_sensor.py
@@ -9,7 +9,7 @@ import requests_mock
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 @@ class TestRestBinarySensorSetup(unittest.TestCase):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -170,7 +170,7 @@ class TestRestBinarySensorSetup(unittest.TestCase):
"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 4351239064a..5ffa12c6167 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -12,7 +12,12 @@ import requests_mock
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 @@ class TestRestSensorSetup(unittest.TestCase):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -164,7 +169,7 @@ class TestRestSensorSetup(unittest.TestCase):
"authentication": "basic",
"username": "my username",
"password": "my password",
- "headers": {"Accept": "application/json"},
+ "headers": {"Accept": CONTENT_TYPE_JSON},
}
},
)
@@ -212,7 +217,7 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
"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 @@ class TestRestSensor(unittest.TestCase):
"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 065645fffd1..47eec8e700f 100644
--- a/tests/components/rest/test_switch.py
+++ b/tests/components/rest/test_switch.py
@@ -123,7 +123,7 @@ class TestRestSwitchSetup:
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 @@ class TestRestSwitch:
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 0aee8ccfbcc..80ede61be84 100644
--- a/tests/components/rest_command/test_init.py
+++ b/tests/components/rest_command/test_init.py
@@ -4,6 +4,7 @@ import asyncio
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 @@ class TestRestCommandComponent:
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 @@ class TestRestCommandComponent:
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 1468037b70d..d18076a372a 100644
--- a/tests/components/rflink/test_sensor.py
+++ b/tests/components/rflink/test_sensor.py
@@ -12,7 +12,12 @@ from homeassistant.components.rflink import (
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/test_sensor.py b/tests/components/rfxtrx/test_sensor.py
index 18239550a85..d0100e4ea14 100644
--- a/tests/components/rfxtrx/test_sensor.py
+++ b/tests/components/rfxtrx/test_sensor.py
@@ -2,7 +2,7 @@
import pytest
from homeassistant.components.rfxtrx.const import ATTR_EVENT
-from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS
from homeassistant.core import State
from homeassistant.setup import async_setup_component
@@ -35,7 +35,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(
@@ -75,31 +75,31 @@ 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) == "dBm"
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):
@@ -127,7 +127,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 +136,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 +145,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 +159,27 @@ 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) == "dBm"
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 +188,27 @@ 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) == "dBm"
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,27 +217,27 @@ 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) == "dBm"
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
@@ -314,13 +314,13 @@ 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) == "dBm"
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) == "dBm"
await rfxtrx.signal("0913000022670e013b70")
await rfxtrx.signal("0b1100cd0213c7f230010f71")
diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py
index e9d5091d664..b4ce1811c91 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 00000000000..25705bd854f
--- /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 00000000000..873f654aa3b
--- /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 00000000000..090b6a6a793
--- /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/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py
index e9967faee91..03eb907d09b 100644
--- a/tests/components/shelly/test_config_flow.py
+++ b/tests/components/shelly/test_config_flow.py
@@ -26,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
@@ -73,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
@@ -210,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"],
@@ -238,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
@@ -285,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"],
@@ -370,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/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py
index 86cd0fc384c..e3d0b0479c4 100644
--- a/tests/components/simplisafe/test_config_flow.py
+++ b/tests/components/simplisafe/test_config_flow.py
@@ -10,7 +10,7 @@ from homeassistant.components.simplisafe import DOMAIN
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/sma/test_sensor.py b/tests/components/sma/test_sensor.py
index 2c317520a7d..f12af1f3849 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/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index b1df938b3b1..89b4fbe9b13 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 b076f39d0d2..a66184887d5 100644
--- a/tests/components/sonarr/__init__.py
+++ b/tests/components/sonarr/__init__.py
@@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_SSL,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
from homeassistant.helpers.typing import HomeAssistantType
@@ -34,6 +35,8 @@ MOCK_SENSOR_CONFIG = {
"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 15bd13b580d..2c39e4384e5 100644
--- a/tests/components/sonarr/test_config_flow.py
+++ b/tests/components/sonarr/test_config_flow.py
@@ -6,8 +6,8 @@ from homeassistant.components.sonarr.const import (
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 homeassistant.helpers.typing import HomeAssistantType
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 e9f01290461..258be0203bb 100644
--- a/tests/components/sonarr/test_init.py
+++ b/tests/components/sonarr/test_init.py
@@ -3,8 +3,11 @@ from homeassistant.components.sonarr.const import DOMAIN
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 b831de56b05..0f33434254c 100644
--- a/tests/components/spaceapi/test_init.py
+++ b/tests/components/spaceapi/test_init.py
@@ -5,7 +5,7 @@ from unittest.mock import patch
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/splunk/__init__.py b/tests/components/splunk/__init__.py
deleted file mode 100644
index 709483291e3..00000000000
--- 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 86de865bc0d..00000000000
--- 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/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index 6e36778b75d..b6c8266b5da 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 e1d658d05b7..511061933cb 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 e60c5c2e9a5..24401963974 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -115,7 +115,7 @@ class TestStatisticsSensor(unittest.TestCase):
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/sun/test_trigger.py b/tests/components/sun/test_trigger.py
index f730dae3cf1..2cb21051200 100644
--- a/tests/components/sun/test_trigger.py
+++ b/tests/components/sun/test_trigger.py
@@ -5,13 +5,19 @@ import pytest
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/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py
index 64527c70964..738a5bed332 100644
--- a/tests/components/synology_dsm/test_config_flow.py
+++ b/tests/components/synology_dsm/test_config_flow.py
@@ -19,6 +19,7 @@ from homeassistant.components.synology_dsm.const import (
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 @@ from homeassistant.const import (
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SSL,
+ CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.helpers.typing import HomeAssistantType
@@ -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 d3f7447277c..49cd2d8ea8b 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/teksavvy/__init__.py b/tests/components/teksavvy/__init__.py
deleted file mode 100644
index 8c8a0fc82ca..00000000000
--- 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 b0de95d72d1..00000000000
--- 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 5deb540782c..7202d8a3a1b 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_sensor.py b/tests/components/template/test_sensor.py
index 6c9bfa7e632..8f582f01ef5 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -1,5 +1,6 @@
"""The test for the Template sensor platform."""
from asyncio import Event
+from datetime import timedelta
from unittest.mock import patch
from homeassistant.bootstrap import async_from_config_dict
@@ -17,7 +18,11 @@ from homeassistant.helpers.template import Template
from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component
import homeassistant.util.dt as dt_util
-from tests.common import assert_setup_component, get_test_home_assistant
+from tests.common import (
+ assert_setup_component,
+ async_fire_time_changed,
+ get_test_home_assistant,
+)
class TestTemplateSensor:
@@ -792,9 +797,9 @@ async def test_self_referencing_sensor_loop(hass, caplog):
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
- assert int(state.state) == 1
+ assert int(state.state) == 2
await hass.async_block_till_done()
- assert int(state.state) == 1
+ assert int(state.state) == 2
async def test_self_referencing_sensor_with_icon_loop(hass, caplog):
@@ -828,11 +833,11 @@ async def test_self_referencing_sensor_with_icon_loop(hass, caplog):
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
- assert int(state.state) == 2
+ assert int(state.state) == 3
assert state.attributes[ATTR_ICON] == "mdi:greater"
await hass.async_block_till_done()
- assert int(state.state) == 2
+ assert int(state.state) == 3
async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog):
@@ -867,12 +872,12 @@ async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, c
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
- assert int(state.state) == 3
+ 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) == 3
+ assert int(state.state) == 4
async def test_self_referencing_entity_picture_loop(hass, caplog):
@@ -900,14 +905,19 @@ async def test_self_referencing_entity_picture_loop(hass, caplog):
assert len(hass.states.async_all()) == 1
- await hass.async_block_till_done()
- 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()
+ 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] == "1"
+ assert state.attributes[ATTR_ENTITY_PICTURE] == "2"
await hass.async_block_till_done()
assert int(state.state) == 1
diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py
index 300173fdadf..4bda4dc23ca 100644
--- a/tests/components/template/test_trigger.py
+++ b/tests/components/template/test_trigger.py
@@ -6,6 +6,7 @@ import pytest
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 @@ from tests.common import (
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/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py
index 7dd19e755f3..d176c6b1ee4 100644
--- a/tests/components/twentemilieu/test_config_flow.py
+++ b/tests/components/twentemilieu/test_config_flow.py
@@ -9,7 +9,7 @@ from homeassistant.components.twentemilieu.const import (
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/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py
index a1af12dfb76..8b935d7744b 100644
--- a/tests/components/unifi/test_config_flow.py
+++ b/tests/components/unifi/test_config_flow.py
@@ -4,6 +4,7 @@ import aiounifi
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 @@ from homeassistant.const import (
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 5600f454336..5fee4a85f9a 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -13,6 +13,7 @@ from homeassistant.components.unifi.const import (
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 @@ CONTROLLER_HOST = {
"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 2d5fbe96e0f..690b9d77899 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -8,10 +8,12 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
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 @@ CLIENTS = [
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
+ "uptime": 1600094505,
},
{
"hostname": "Wireless client hostname",
@@ -42,6 +45,7 @@ CLIENTS = [
"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/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py
index 98df710d8fe..997811a835f 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 7116077177a..f946ce352a5 100644
--- a/tests/components/utility_meter/test_init.py
+++ b/tests/components/utility_meter/test_init.py
@@ -12,6 +12,7 @@ from homeassistant.components.utility_meter.const import (
)
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 c1613c53a20..1fff168b748 100644
--- a/tests/components/utility_meter/test_sensor.py
+++ b/tests/components/utility_meter/test_sensor.py
@@ -13,6 +13,7 @@ from homeassistant.components.utility_meter.const import (
)
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 3bccacc0a94..0a7bc4489c7 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 31e7c706ec9..29c6a0e8683 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 @@ class ComponentFactory:
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 @@ class ComponentFactory:
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 @@ class ComponentFactory:
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 @@ class ComponentFactory:
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 4b0d41d9a1e..a02c2ef1635 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 f11f3ea5a3b..370ecc18dcd 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 @@ async def test_climate_f(
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 793e313125c..dceac728e4d 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 311c8013d86..f3dc2263749 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 210037a2ca3..b3f7b3249ef 100644
--- a/tests/components/vera/test_init.py
+++ b/tests/components/vera/test_init.py
@@ -12,10 +12,10 @@ from homeassistant.components.vera import (
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,7 +51,7 @@ 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"
@@ -60,7 +60,7 @@ async def test_init_from_file(
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,),
),
@@ -69,6 +69,62 @@ async def test_init_from_file(
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_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 99391d8d82a..72118e33a31 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 11af1f5a7b7..b3433b2bafb 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 29ef338b9f1..6c80f27d8c8 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 36730e8d6d2..3d6b11b0685 100644
--- a/tests/components/vera/test_sensor.py
+++ b/tests/components/vera/test_sensor.py
@@ -3,7 +3,7 @@ from typing import Any, Callable, Tuple
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 60e31add4bd..42c74e4e843 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/vizio/conftest.py b/tests/components/vizio/conftest.py
index 08e5da5c9e5..c8a9083bb1a 100644
--- a/tests/components/vizio/conftest.py
+++ b/tests/components/vizio/conftest.py
@@ -22,7 +22,7 @@ from .const import (
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/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py
index 1b9eea86018..3969ff90706 100644
--- a/tests/components/websocket_api/test_commands.py
+++ b/tests/components/websocket_api/test_commands.py
@@ -397,9 +397,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 +435,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 +473,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 00000000000..832b72c5c1c
--- /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/wled/__init__.py b/tests/components/wled/__init__.py
index 487ccd9ab9e..a39d1ef6453 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 a0c5c11e7ce..7793dc2a378 100644
--- a/tests/components/wled/test_config_flow.py
+++ b/tests/components/wled/test_config_flow.py
@@ -5,7 +5,7 @@ from wled import WLEDConnectionError
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()
@@ -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 f4efea1b57d..39cde51dbfd 100644
--- a/tests/components/wled/test_sensor.py
+++ b/tests/components/wled/test_sensor.py
@@ -183,7 +183,6 @@ async def test_disabled_by_default_sensors(
"""Test the disabled by default WLED sensors."""
await init_integration(hass, aioclient_mock)
registry = await hass.helpers.entity_registry.async_get_registry()
- print(registry.entities)
state = hass.states.get(entity_id)
assert state is None
diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py
index b4fb30d25c5..8709f5b6a46 100644
--- a/tests/components/wunderground/test_sensor.py
+++ b/tests/components/wunderground/test_sensor.py
@@ -3,7 +3,12 @@ import aiohttp
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/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py
deleted file mode 100644
index e7d7b030aaf..00000000000
--- 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 ae1f6d5fd98..8767953b363 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -9,7 +9,11 @@ from zeroconf import (
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 e45a93a38b3..9cf953f4c8d 100644
--- a/tests/components/zeroconf/test_usage.py
+++ b/tests/components/zeroconf/test_usage.py
@@ -3,12 +3,16 @@ import zeroconf
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 82799e8dd9d..11f390b202e 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -108,6 +108,7 @@ class FakeDevice:
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 0587bd14c8c..53861bc0d9a 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 afa86e90f2c..7a2217521ad 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 @@ from .common import (
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_IAS = {
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 471e44f8409..8a6d934d373 100644
--- a/tests/components/zha/test_channels.py
+++ b/tests/components/zha/test_channels.py
@@ -3,6 +3,7 @@ import asyncio
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 c6c0e74050d..b295543b3e8 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 83a57f1fabf..1cc9fb27d89 100644
--- a/tests/components/zha/test_device.py
+++ b/tests/components/zha/test_device.py
@@ -4,6 +4,7 @@ import time
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 zigpy_device(zigpy_device_mock):
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 zigpy_device_mains(zigpy_device_mock):
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 40e64934f89..de3a4eb1296 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 e0a73903e0d..801b6831379 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -3,6 +3,7 @@ from datetime import timedelta
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 0cd8c58e49f..5589a7d94ac 100644
--- a/tests/components/zha/test_discover.py
+++ b/tests/components/zha/test_discover.py
@@ -4,6 +4,7 @@ import re
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 b163edbd49a..b12b9249373 100644
--- a/tests/components/zha/test_fan.py
+++ b/tests/components/zha/test_fan.py
@@ -43,7 +43,11 @@ IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8"
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 cc9e811cb49..718c7cdba19 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 bd340445527..642504384cc 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -34,7 +34,7 @@ IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7"
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_ON_OFF = {
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_LEVEL = {
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 7cba51b3e5c..8d854894ba0 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 @@ from homeassistant.const import (
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 b1c0c643bbc..aab8dafef4f 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/zodiac/__init__.py b/tests/components/zodiac/__init__.py
new file mode 100644
index 00000000000..e9bae20c442
--- /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 00000000000..b08352dd2c0
--- /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 e80f70b10fe..0477f9bead7 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/conftest.py b/tests/conftest.py
index 9008359e539..d6da979bb5e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -395,6 +395,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/homekit_controller/aqara_switch.json b/tests/fixtures/homekit_controller/aqara_switch.json
new file mode 100644
index 00000000000..320478343f4
--- /dev/null
+++ b/tests/fixtures/homekit_controller/aqara_switch.json
@@ -0,0 +1,209 @@
+[
+ {
+ "aid": 1,
+ "services": [
+ {
+ "characteristics": [
+ {
+ "format": "bool",
+ "iid": 65537,
+ "perms": [
+ "pw"
+ ],
+ "type": "00000014-0000-1000-8000-0026BB765291"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65538,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000020-0000-1000-8000-0026BB765291",
+ "value": "Aqara"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65539,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000021-0000-1000-8000-0026BB765291",
+ "value": "AR004"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65540,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "value": "Programmable Switch"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65541,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000030-0000-1000-8000-0026BB765291",
+ "value": "111a1111a1a111"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65542,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000052-0000-1000-8000-0026BB765291",
+ "value": "9"
+ },
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 65543,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000053-0000-1000-8000-0026BB765291",
+ "value": "1.0"
+ }
+ ],
+ "hidden": false,
+ "iid": 1,
+ "linked": [],
+ "primary": false,
+ "stype": "accessory-information",
+ "type": "0000003E-0000-1000-8000-0026BB765291"
+ },
+ {
+ "characteristics": [
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 262146,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "value": "Programmable Switch"
+ },
+ {
+ "ev": false,
+ "format": "uint8",
+ "iid": 262147,
+ "maxValue": 2,
+ "minStep": 1,
+ "minValue": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "type": "00000073-0000-1000-8000-0026BB765291",
+ "valid-values": [
+ 0,
+ 1,
+ 2
+ ],
+ "value": null
+ },
+ {
+ "ev": false,
+ "format": "uint8",
+ "iid": 262148,
+ "maxValue": 255,
+ "minStep": 1,
+ "minValue": 1,
+ "perms": [
+ "pr"
+ ],
+ "type": "000000CB-0000-1000-8000-0026BB765291",
+ "value": 1
+ }
+ ],
+ "hidden": false,
+ "iid": 4,
+ "linked": [],
+ "primary": true,
+ "stype": "stateless-programmable-switch",
+ "type": "00000089-0000-1000-8000-0026BB765291"
+ },
+ {
+ "characteristics": [
+ {
+ "ev": false,
+ "format": "string",
+ "iid": 327682,
+ "perms": [
+ "pr"
+ ],
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "value": "Battery Sensor"
+ },
+ {
+ "ev": true,
+ "format": "uint8",
+ "iid": 327683,
+ "maxValue": 100,
+ "minStep": 1,
+ "minValue": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "type": "00000068-0000-1000-8000-0026BB765291",
+ "unit": "percentage",
+ "value": 100
+ },
+ {
+ "ev": true,
+ "format": "uint8",
+ "iid": 327685,
+ "maxValue": 1,
+ "minStep": 1,
+ "minValue": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "type": "00000079-0000-1000-8000-0026BB765291",
+ "valid-values": [
+ 0,
+ 1
+ ],
+ "value": 0
+ },
+ {
+ "ev": true,
+ "format": "uint8",
+ "iid": 327684,
+ "maxValue": 2,
+ "minStep": 1,
+ "minValue": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "type": "0000008F-0000-1000-8000-0026BB765291",
+ "valid-values": [
+ 0,
+ 1,
+ 2
+ ],
+ "value": 2
+ }
+ ],
+ "hidden": false,
+ "iid": 5,
+ "linked": [],
+ "primary": false,
+ "stype": "battery",
+ "type": "00000096-0000-1000-8000-0026BB765291"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/events.txt b/tests/fixtures/netatmo/events.txt
new file mode 100644
index 00000000000..f2bc29f782c
--- /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 370edc15be1..99cef9091c5 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/helpers/test_condition.py b/tests/helpers/test_condition.py
index 71770d21186..001c59e2f2c 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -422,7 +422,7 @@ async def test_state_attribute(hass):
"condition": "state",
"entity_id": "sensor.temperature",
"attribute": "attribute1",
- "state": "200",
+ "state": 200,
},
],
},
@@ -435,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)
@@ -444,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(
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index ced25f1ad5b..bb0d17d7b0e 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -14,6 +14,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.event import (
+ TrackStates,
TrackTemplate,
TrackTemplateResult,
async_call_later,
@@ -23,6 +24,8 @@ from homeassistant.helpers.event import (
async_track_state_added_domain,
async_track_state_change,
async_track_state_change_event,
+ async_track_state_change_filtered,
+ async_track_state_removed_domain,
async_track_sunrise,
async_track_sunset,
async_track_template,
@@ -254,6 +257,142 @@ async def test_track_state_change(hass):
assert len(wildercard_runs) == 6
+async def test_async_track_state_change_filtered(hass):
+ """Test async_track_state_change_filtered."""
+ single_entity_id_tracker = []
+ multiple_entity_id_tracker = []
+
+ @ha.callback
+ def single_run_callback(event):
+ old_state = event.data.get("old_state")
+ new_state = event.data.get("new_state")
+
+ single_entity_id_tracker.append((old_state, new_state))
+
+ @ha.callback
+ def multiple_run_callback(event):
+ old_state = event.data.get("old_state")
+ new_state = event.data.get("new_state")
+
+ multiple_entity_id_tracker.append((old_state, new_state))
+
+ @ha.callback
+ def callback_that_throws(event):
+ raise ValueError
+
+ track_single = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, None), single_run_callback
+ )
+ assert track_single.listeners == {
+ "all": False,
+ "domains": None,
+ "entities": {"light.bowl"},
+ }
+
+ track_multi = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, {"switch"}), multiple_run_callback
+ )
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+
+ track_throws = async_track_state_change_filtered(
+ hass, TrackStates(False, {"light.bowl"}, {"switch"}), callback_that_throws
+ )
+ assert track_throws.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+
+ # Adding state to state machine
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert single_entity_id_tracker[-1][0] is None
+ assert single_entity_id_tracker[-1][1] is not None
+ assert len(multiple_entity_id_tracker) == 1
+ assert multiple_entity_id_tracker[-1][0] is None
+ assert multiple_entity_id_tracker[-1][1] is not None
+
+ # Set same state should not trigger a state change/listener
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert len(multiple_entity_id_tracker) == 1
+
+ # State change off -> on
+ hass.states.async_set("light.Bowl", "off")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 2
+ assert len(multiple_entity_id_tracker) == 2
+
+ # State change off -> off
+ hass.states.async_set("light.Bowl", "off", {"some_attr": 1})
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 3
+ assert len(multiple_entity_id_tracker) == 3
+
+ # State change off -> on
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 4
+ assert len(multiple_entity_id_tracker) == 4
+
+ hass.states.async_remove("light.bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert single_entity_id_tracker[-1][0] is not None
+ assert single_entity_id_tracker[-1][1] is None
+ assert len(multiple_entity_id_tracker) == 5
+ assert multiple_entity_id_tracker[-1][0] is not None
+ assert multiple_entity_id_tracker[-1][1] is None
+
+ # Set state for different entity id
+ hass.states.async_set("switch.kitchen", "on")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert len(multiple_entity_id_tracker) == 6
+
+ track_single.async_remove()
+ # Ensure unsubing the listener works
+ hass.states.async_set("light.Bowl", "off")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 5
+ assert len(multiple_entity_id_tracker) == 7
+
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": {"switch"},
+ "entities": {"light.bowl"},
+ }
+ track_multi.async_update_listeners(TrackStates(False, {"light.bowl"}, None))
+ assert track_multi.listeners == {
+ "all": False,
+ "domains": None,
+ "entities": {"light.bowl"},
+ }
+ hass.states.async_set("light.Bowl", "on")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+ hass.states.async_set("switch.kitchen", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+
+ track_multi.async_update_listeners(TrackStates(True, None, None))
+ hass.states.async_set("switch.kitchen", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 8
+ hass.states.async_set("switch.any", "off")
+ await hass.async_block_till_done()
+ assert len(multiple_entity_id_tracker) == 9
+
+ track_multi.async_remove()
+ track_throws.async_remove()
+
+
async def test_async_track_state_change_event(hass):
"""Test async_track_state_change_event."""
single_entity_id_tracker = []
@@ -429,6 +568,132 @@ async def test_async_track_state_added_domain(hass):
unsub_throws()
+async def test_async_track_state_removed_domain(hass):
+ """Test async_track_state_removed_domain."""
+ 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
+
+ unsub_single = async_track_state_removed_domain(hass, "light", single_run_callback)
+ unsub_multi = async_track_state_removed_domain(
+ hass, ["light", "switch"], multiple_run_callback
+ )
+ unsub_throws = async_track_state_removed_domain(
+ hass, ["light", "switch"], callback_that_throws
+ )
+
+ # Adding state to state machine
+ hass.states.async_set("light.Bowl", "on")
+ hass.states.async_remove("light.Bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert single_entity_id_tracker[-1][1] is None
+ assert single_entity_id_tracker[-1][0] is not None
+ assert len(multiple_entity_id_tracker) == 1
+ assert multiple_entity_id_tracker[-1][1] is None
+ assert multiple_entity_id_tracker[-1][0] is not None
+
+ # Added and than removed (light)
+ hass.states.async_set("light.Bowl", "on")
+ hass.states.async_remove("light.Bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 2
+ assert len(multiple_entity_id_tracker) == 2
+
+ # Added and than removed (light)
+ hass.states.async_set("light.Bowl", "off")
+ hass.states.async_remove("light.Bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 3
+ assert len(multiple_entity_id_tracker) == 3
+
+ # Added and than removed (light)
+ hass.states.async_set("light.Bowl", "off", {"some_attr": 1})
+ hass.states.async_remove("light.Bowl")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 4
+ assert len(multiple_entity_id_tracker) == 4
+
+ # Added and than removed (switch)
+ hass.states.async_set("switch.kitchen", "on")
+ hass.states.async_remove("switch.kitchen")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 4
+ assert len(multiple_entity_id_tracker) == 5
+
+ unsub_single()
+ # Ensure unsubing the listener works
+ hass.states.async_set("light.new", "off")
+ hass.states.async_remove("light.new")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 4
+ assert len(multiple_entity_id_tracker) == 6
+
+ unsub_multi()
+ unsub_throws()
+
+
+async def test_async_track_state_removed_domain_match_all(hass):
+ """Test async_track_state_removed_domain with a match_all."""
+ single_entity_id_tracker = []
+ match_all_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 match_all_run_callback(event):
+ old_state = event.data.get("old_state")
+ new_state = event.data.get("new_state")
+
+ match_all_entity_id_tracker.append((old_state, new_state))
+
+ unsub_single = async_track_state_removed_domain(hass, "light", single_run_callback)
+ unsub_match_all = async_track_state_removed_domain(
+ hass, MATCH_ALL, match_all_run_callback
+ )
+ hass.states.async_set("light.new", "off")
+ hass.states.async_remove("light.new")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert len(match_all_entity_id_tracker) == 1
+
+ hass.states.async_set("switch.new", "off")
+ hass.states.async_remove("switch.new")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert len(match_all_entity_id_tracker) == 2
+
+ unsub_match_all()
+ unsub_single()
+ hass.states.async_set("switch.new", "off")
+ hass.states.async_remove("switch.new")
+ await hass.async_block_till_done()
+ assert len(single_entity_id_tracker) == 1
+ assert len(match_all_entity_id_tracker) == 2
+
+
async def test_track_template(hass):
"""Test tracking template."""
specific_runs = []
@@ -662,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" %}
@@ -683,7 +947,9 @@ async def test_track_template_result_complex(hass):
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()
@@ -897,6 +1163,8 @@ async def test_track_template_result_with_group(hass):
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)
@@ -979,6 +1247,7 @@ async def test_track_template_result_iterator(hass):
hass,
),
None,
+ timedelta(seconds=0),
)
],
iterator_callback,
@@ -1006,6 +1275,7 @@ async def test_track_template_result_iterator(hass):
hass,
),
None,
+ timedelta(seconds=0),
)
],
filter_callback,
@@ -1014,7 +1284,7 @@ async def test_track_template_result_iterator(hass):
assert info.listeners == {
"all": False,
"domains": {"sensor"},
- "entities": {"sensor.test"},
+ "entities": set(),
}
hass.states.async_set("sensor.test", 6)
@@ -1132,6 +1402,268 @@ async def test_static_string(hass):
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)
@@ -1285,7 +1817,7 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(hass
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,
)
@@ -1333,6 +1865,25 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(hass
]
+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 ed97b3e3757..495c9d511bd 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 00000000000..c34de9586dd
--- /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 0bd353e1fa0..93bb249c485 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -16,6 +16,7 @@ import homeassistant.components.scene as scene
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 1a0cdb79bbc..929df2a32e0 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 c1f018b47d6..5535fa53993 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -8,6 +8,7 @@ import pytz
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.const import (
)
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"],
)
@@ -892,6 +913,65 @@ def test_relative_time(mock_is_safe, hass):
)
+@patch(
+ "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
+ return_value=True,
+)
+def test_timedelta(mock_is_safe, hass):
+ """Test relative_time method."""
+ now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
+ with patch("homeassistant.util.dt.now", return_value=now):
+ assert (
+ "0:02:00"
+ == template.Template(
+ "{{timedelta(seconds=120)}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "1 day, 0:00:00"
+ == template.Template(
+ "{{timedelta(seconds=86400)}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "1 day, 4:00:00"
+ == template.Template(
+ "{{timedelta(days=1, hours=4)}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "1 hour"
+ == template.Template(
+ "{{relative_time(now() - timedelta(seconds=3600))}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "1 day"
+ == template.Template(
+ "{{relative_time(now() - timedelta(seconds=86400))}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "1 day"
+ == template.Template(
+ "{{relative_time(now() - timedelta(seconds=86401))}}",
+ hass,
+ ).async_render()
+ )
+ assert (
+ "15 days"
+ == template.Template(
+ "{{relative_time(now() - timedelta(weeks=2, days=1))}}",
+ hass,
+ ).async_render()
+ )
+
+
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
@@ -1272,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):
@@ -1297,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")
@@ -1324,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(
@@ -1343,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,
@@ -1364,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"]
)
@@ -1381,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):
@@ -1446,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,
@@ -1457,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):
@@ -1493,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 %}
@@ -1511,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):
@@ -1540,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 = """
@@ -1575,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):
@@ -1594,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):
@@ -1872,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,
@@ -1884,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):
@@ -1898,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")
@@ -1907,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):
@@ -2032,6 +2116,8 @@ states.sensor.pick_humidity.state ~ " %"
)
)
+ 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(
@@ -2212,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)
@@ -2321,3 +2407,204 @@ async def test_protected_blocked(hass):
tmp = template.Template('{{ states.sensor.any.__getattr__("any") }}', hass)
with pytest.raises(TemplateError):
tmp.async_render()
+
+
+async def test_demo_template(hass):
+ """Test the demo template works as expected."""
+ 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")
+
+ demo_template_str = """
+{## Imitate available variables: ##}
+{% set my_test_json = {
+ "temperature": 25,
+ "unit": "°C"
+} %}
+
+The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
+
+{% if is_state("sun.sun", "above_horizon") -%}
+ The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago.
+{%- else -%}
+ The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}.
+{%- endif %}
+
+For loop example getting 3 entity values:
+
+{% for states in states | slice(3) -%}
+ {% set state = states | first %}
+ {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
+ {{ state.name | lower }} is {{state.state_with_unit}}
+{%- endfor %}.
+"""
+ tmp = template.Template(demo_template_str, hass)
+
+ result = tmp.async_render()
+ assert "The temperature is 25" in result
+ assert "is on" in result
+ 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 1049108f9de..41c0aa5727b 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 fb22ee1118e..181e80da30c 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 = {
diff --git a/tests/test_core.py b/tests/test_core.py
index f5de9c5f1a1..33cb0a37e23 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -268,49 +268,47 @@ 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_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)
+ ]
- 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)
- ]
+ assert event1 == event2
- assert event1 == event2
- def test_repr(self):
- """Test that repr method works."""
- assert str(ha.Event("TestEvent")) == ""
+def test_event_repr():
+ """Test that Event repr method works."""
+ assert str(ha.Event("TestEvent")) == ""
- assert (
- str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote))
- == ""
- )
+ assert (
+ str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote))
+ == ""
+ )
- def test_as_dict(self):
- """Test 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,
- "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 as 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,
+ "context": {
+ "id": event.context.id,
+ "parent_id": None,
+ "user_id": event.context.user_id,
+ },
+ }
+ assert event.as_dict() == expected
class TestEventBus(unittest.TestCase):
@@ -1477,3 +1475,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 8d0cdf6ac93..d9ed47844af 100644
--- a/tests/testing_config/custom_components/test/sensor.py
+++ b/tests/testing_config/custom_components/test/sensor.py
@@ -4,7 +4,7 @@ Provide a mock sensor platform.
Call init before using it in your tests to ensure clean test data.
"""
import homeassistant.components.sensor as sensor
-from homeassistant.const import PERCENTAGE
+from homeassistant.const import PERCENTAGE, PRESSURE_HPA
from tests.common import MockEntity
@@ -18,11 +18,11 @@ UNITS_OF_MEASUREMENT = {
sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # 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 00000000000..d5f05f5c93e
--- /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 7829a7a98d7..cc1df307bfe 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}