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 ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)" + "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", + "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"